From 6fd0df6795c10f7cdb4e67f62c6a63a059d72a4b Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Fri, 18 Nov 2022 22:04:09 +0100 Subject: [PATCH 1/4] Add haveibeenpwned check --- .../fr/xephi/authme/message/MessageKey.java | 3 + .../authme/service/ValidationService.java | 89 +++++++++++++++++-- .../settings/properties/SecuritySettings.java | 9 ++ src/main/resources/messages/messages_en.yml | 1 + .../authme/service/ValidationServiceTest.java | 2 + 5 files changed, 99 insertions(+), 5 deletions(-) diff --git a/src/main/java/fr/xephi/authme/message/MessageKey.java b/src/main/java/fr/xephi/authme/message/MessageKey.java index c7ed8dc35f..02b7c302fd 100644 --- a/src/main/java/fr/xephi/authme/message/MessageKey.java +++ b/src/main/java/fr/xephi/authme/message/MessageKey.java @@ -80,6 +80,9 @@ public enum MessageKey { /** The chosen password isn't safe, please choose another one... */ PASSWORD_UNSAFE_ERROR("password.unsafe_password"), + /** Your chosen password has been used %pwned_count times worldwide! Please use a strong password... */ + PASSWORD_PWNED_ERROR("password.pwned_password", "%pwned_count"), + /** Your password contains illegal characters. Allowed chars: %valid_chars */ PASSWORD_CHARACTERS_ERROR("password.forbidden_characters", "%valid_chars"), diff --git a/src/main/java/fr/xephi/authme/service/ValidationService.java b/src/main/java/fr/xephi/authme/service/ValidationService.java index 536d2111ee..3c57842fe9 100644 --- a/src/main/java/fr/xephi/authme/service/ValidationService.java +++ b/src/main/java/fr/xephi/authme/service/ValidationService.java @@ -6,10 +6,11 @@ import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.initialization.Reloadable; -import fr.xephi.authme.output.ConsoleLoggerFactory; import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.output.ConsoleLoggerFactory; import fr.xephi.authme.permission.PermissionsManager; import fr.xephi.authme.permission.PlayerStatePermission; +import fr.xephi.authme.security.HashUtils; import fr.xephi.authme.settings.Settings; import fr.xephi.authme.settings.properties.EmailSettings; import fr.xephi.authme.settings.properties.ProtectionSettings; @@ -23,6 +24,10 @@ import javax.annotation.PostConstruct; import javax.inject.Inject; +import java.io.DataInputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Locale; @@ -35,7 +40,7 @@ * Validation service. */ public class ValidationService implements Reloadable { - + private final ConsoleLogger logger = ConsoleLoggerFactory.get(ValidationService.class); @Inject @@ -80,7 +85,16 @@ public ValidationResult validatePassword(String password, String username) { return new ValidationResult(MessageKey.INVALID_PASSWORD_LENGTH); } else if (settings.getProperty(SecuritySettings.UNSAFE_PASSWORDS).contains(passLow)) { return new ValidationResult(MessageKey.PASSWORD_UNSAFE_ERROR); + } else if (settings.getProperty(SecuritySettings.HAVE_I_BEEN_PWNED_CHECK)) { + HaveIBeenPwnedResults results = validatePasswordHaveIBeenPwned(password); + + if (results != null + && results.isPwned() + && results.getPwnCount() > settings.getProperty(SecuritySettings.HAVE_I_BEEN_PWNED_LIMIT)) { + return new ValidationResult(MessageKey.PASSWORD_PWNED_ERROR, String.valueOf(results.getPwnCount())); + } } + return new ValidationResult(); } @@ -103,7 +117,7 @@ public boolean validateEmail(String email) { * Queries the database whether the email is still free for registration, i.e. whether the given * command sender may use the email to register a new account (as defined by settings and permissions). * - * @param email the email to verify + * @param email the email to verify * @param sender the command sender * @return true if the email may be used, false otherwise (registration threshold has been exceeded) */ @@ -178,7 +192,7 @@ public boolean fulfillsNameRestrictions(Player player) { * Whitelist has precedence over blacklist: if a whitelist is set, the value is rejected if not present * in the whitelist. * - * @param value the value to verify + * @param value the value to verify * @param whitelist the whitelist property * @param blacklist the blacklist property * @return true if the value is admitted by the lists, false otherwise @@ -222,6 +236,53 @@ private Multimap loadNameRestrictions(Set configuredRest return restrictions; } + /** + * Check haveibeenpwned.com for the given password. + * + * @param password password to check for + * @return Results of the check + */ + public HaveIBeenPwnedResults validatePasswordHaveIBeenPwned(String password) { + String hash = HashUtils.sha1(password); + + String hashPrefix = hash.substring(0, 5); + + try { + String url = String.format("https://api.pwnedpasswords.com/range/%s", hashPrefix); + HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); + connection.setRequestMethod("GET"); + connection.setRequestProperty("User-Agent", "AuthMeReloaded"); + connection.setConnectTimeout(5000); + connection.setReadTimeout(5000); + connection.setDoOutput(true); + connection.setDoInput(true); + StringBuilder outStr = new StringBuilder(); + + try (DataInputStream input = new DataInputStream(connection.getInputStream())) { + for (int c = input.read(); c != -1; c = input.read()) + outStr.append((char) c); + } + + String[] hashes = outStr.toString().split("\n"); + System.out.println(Arrays.toString(hashes)); + for (String hashSuffix : hashes) { + String[] hashSuffixParts = hashSuffix.trim().split(":"); + System.out.println(Arrays.toString(hashSuffixParts)); + if (hashSuffixParts[0].equalsIgnoreCase(hash.substring(5))) { + System.out.println("Found match"); + System.out.println(hashSuffixParts[1]); + return new HaveIBeenPwnedResults(true, Integer.parseInt(hashSuffixParts[1])); + } + } + + return new HaveIBeenPwnedResults(false, 0); + } catch (Exception e) { + e.printStackTrace(); + } + + return null; + } + public static final class ValidationResult { private final MessageKey messageKey; private final String[] args; @@ -238,7 +299,7 @@ public ValidationResult() { * Constructor for a failed validation. * * @param messageKey message key of the validation error - * @param args arguments for the message key + * @param args arguments for the message key */ public ValidationResult(MessageKey messageKey, String... args) { this.messageKey = messageKey; @@ -262,4 +323,22 @@ public String[] getArgs() { return args; } } + + public static final class HaveIBeenPwnedResults { + private final boolean isPwned; + private final int pwnCount; + + public HaveIBeenPwnedResults(boolean isPwned, int pwnCount) { + this.isPwned = isPwned; + this.pwnCount = pwnCount; + } + + public boolean isPwned() { + return isPwned; + } + + public int getPwnCount() { + return pwnCount; + } + } } diff --git a/src/main/java/fr/xephi/authme/settings/properties/SecuritySettings.java b/src/main/java/fr/xephi/authme/settings/properties/SecuritySettings.java index 0cffd28047..ec10768563 100644 --- a/src/main/java/fr/xephi/authme/settings/properties/SecuritySettings.java +++ b/src/main/java/fr/xephi/authme/settings/properties/SecuritySettings.java @@ -89,6 +89,15 @@ public final class SecuritySettings implements SettingsHolder { newLowercaseStringSetProperty("settings.security.unsafePasswords", "123456", "password", "qwerty", "12345", "54321", "123456789", "help"); + @Comment({"Query haveibeenpwned.com with a hashed version of the password.", + "This is used to check whether it is safe."}) + public static final Property HAVE_I_BEEN_PWNED_CHECK = + newProperty("settings.security.haveIBeenPwned.check", true); + + @Comment({"If the password is used more than this number of times, it is considered unsafe."}) + public static final Property HAVE_I_BEEN_PWNED_LIMIT = + newProperty("settings.security.haveIBeenPwned.limit", 0); + @Comment("Tempban a user's IP address if they enter the wrong password too many times") public static final Property TEMPBAN_ON_MAX_LOGINS = newProperty("Security.tempban.enableTempban", false); diff --git a/src/main/resources/messages/messages_en.yml b/src/main/resources/messages/messages_en.yml index 47242ce88a..e9e907ffaa 100644 --- a/src/main/resources/messages/messages_en.yml +++ b/src/main/resources/messages/messages_en.yml @@ -18,6 +18,7 @@ password: match_error: '&cPasswords didn''t match, check them again!' name_in_password: '&cYou can''t use your name as password, please choose another one...' unsafe_password: '&cThe chosen password isn''t safe, please choose another one...' + pwned_password: '&cYour chosen password is not secure. It was used %pwned_count times already! Please use a strong password...' forbidden_characters: '&4Your password contains illegal characters. Allowed chars: %valid_chars' wrong_length: '&cYour password is too short or too long! Please try with another one!' diff --git a/src/test/java/fr/xephi/authme/service/ValidationServiceTest.java b/src/test/java/fr/xephi/authme/service/ValidationServiceTest.java index dafc8c3dc2..bae8c02d96 100644 --- a/src/test/java/fr/xephi/authme/service/ValidationServiceTest.java +++ b/src/test/java/fr/xephi/authme/service/ValidationServiceTest.java @@ -58,6 +58,8 @@ public void createService() { given(settings.getProperty(SecuritySettings.MIN_PASSWORD_LENGTH)).willReturn(3); given(settings.getProperty(SecuritySettings.MAX_PASSWORD_LENGTH)).willReturn(20); given(settings.getProperty(SecuritySettings.UNSAFE_PASSWORDS)).willReturn(newHashSet("unsafe", "other-unsafe")); + given(settings.getProperty(SecuritySettings.HAVE_I_BEEN_PWNED_CHECK)).willReturn(true); + given(settings.getProperty(SecuritySettings.HAVE_I_BEEN_PWNED_LIMIT)).willReturn(0); given(settings.getProperty(EmailSettings.MAX_REG_PER_EMAIL)).willReturn(3); given(settings.getProperty(RestrictionSettings.UNRESTRICTED_NAMES)).willReturn(newHashSet("name01", "npc")); given(settings.getProperty(RestrictionSettings.ENABLE_RESTRICTED_USERS)).willReturn(false); From f745262d4e657985942a3d43573faf9de215f5b2 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sat, 19 Nov 2022 09:22:56 +0100 Subject: [PATCH 2/4] Small tweaks --- src/main/java/fr/xephi/authme/message/MessageKey.java | 2 +- src/main/java/fr/xephi/authme/service/ValidationService.java | 5 ----- src/main/resources/messages/messages_en.yml | 2 +- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/main/java/fr/xephi/authme/message/MessageKey.java b/src/main/java/fr/xephi/authme/message/MessageKey.java index 02b7c302fd..e63b06b358 100644 --- a/src/main/java/fr/xephi/authme/message/MessageKey.java +++ b/src/main/java/fr/xephi/authme/message/MessageKey.java @@ -80,7 +80,7 @@ public enum MessageKey { /** The chosen password isn't safe, please choose another one... */ PASSWORD_UNSAFE_ERROR("password.unsafe_password"), - /** Your chosen password has been used %pwned_count times worldwide! Please use a strong password... */ + /** Your chosen password is not secure. It was used %pwned_count times already! Please use a stronger password... */ PASSWORD_PWNED_ERROR("password.pwned_password", "%pwned_count"), /** Your password contains illegal characters. Allowed chars: %valid_chars */ diff --git a/src/main/java/fr/xephi/authme/service/ValidationService.java b/src/main/java/fr/xephi/authme/service/ValidationService.java index 3c57842fe9..9f685e3129 100644 --- a/src/main/java/fr/xephi/authme/service/ValidationService.java +++ b/src/main/java/fr/xephi/authme/service/ValidationService.java @@ -254,7 +254,6 @@ public HaveIBeenPwnedResults validatePasswordHaveIBeenPwned(String password) { connection.setRequestProperty("User-Agent", "AuthMeReloaded"); connection.setConnectTimeout(5000); connection.setReadTimeout(5000); - connection.setDoOutput(true); connection.setDoInput(true); StringBuilder outStr = new StringBuilder(); @@ -264,13 +263,9 @@ public HaveIBeenPwnedResults validatePasswordHaveIBeenPwned(String password) { } String[] hashes = outStr.toString().split("\n"); - System.out.println(Arrays.toString(hashes)); for (String hashSuffix : hashes) { String[] hashSuffixParts = hashSuffix.trim().split(":"); - System.out.println(Arrays.toString(hashSuffixParts)); if (hashSuffixParts[0].equalsIgnoreCase(hash.substring(5))) { - System.out.println("Found match"); - System.out.println(hashSuffixParts[1]); return new HaveIBeenPwnedResults(true, Integer.parseInt(hashSuffixParts[1])); } } diff --git a/src/main/resources/messages/messages_en.yml b/src/main/resources/messages/messages_en.yml index e9e907ffaa..8355221331 100644 --- a/src/main/resources/messages/messages_en.yml +++ b/src/main/resources/messages/messages_en.yml @@ -18,7 +18,7 @@ password: match_error: '&cPasswords didn''t match, check them again!' name_in_password: '&cYou can''t use your name as password, please choose another one...' unsafe_password: '&cThe chosen password isn''t safe, please choose another one...' - pwned_password: '&cYour chosen password is not secure. It was used %pwned_count times already! Please use a strong password...' + pwned_password: '&cYour chosen password is not secure. It was used %pwned_count times already! Please use a stronger password...' forbidden_characters: '&4Your password contains illegal characters. Allowed chars: %valid_chars' wrong_length: '&cYour password is too short or too long! Please try with another one!' From 6cf68a5c5e04e5456305e8fff956138901ec3e57 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sat, 19 Nov 2022 09:31:34 +0100 Subject: [PATCH 3/4] Use message as on haveibeenpwned website --- src/main/java/fr/xephi/authme/message/MessageKey.java | 2 +- src/main/resources/messages/messages_en.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/fr/xephi/authme/message/MessageKey.java b/src/main/java/fr/xephi/authme/message/MessageKey.java index e63b06b358..210cc64467 100644 --- a/src/main/java/fr/xephi/authme/message/MessageKey.java +++ b/src/main/java/fr/xephi/authme/message/MessageKey.java @@ -80,7 +80,7 @@ public enum MessageKey { /** The chosen password isn't safe, please choose another one... */ PASSWORD_UNSAFE_ERROR("password.unsafe_password"), - /** Your chosen password is not secure. It was used %pwned_count times already! Please use a stronger password... */ + /** Your chosen password is not secure. It has been seen %pwned_count times before! Please use a stronger password... */ PASSWORD_PWNED_ERROR("password.pwned_password", "%pwned_count"), /** Your password contains illegal characters. Allowed chars: %valid_chars */ diff --git a/src/main/resources/messages/messages_en.yml b/src/main/resources/messages/messages_en.yml index 8355221331..02bd1b26f2 100644 --- a/src/main/resources/messages/messages_en.yml +++ b/src/main/resources/messages/messages_en.yml @@ -18,7 +18,7 @@ password: match_error: '&cPasswords didn''t match, check them again!' name_in_password: '&cYou can''t use your name as password, please choose another one...' unsafe_password: '&cThe chosen password isn''t safe, please choose another one...' - pwned_password: '&cYour chosen password is not secure. It was used %pwned_count times already! Please use a stronger password...' + pwned_password: '&cYour chosen password is not secure. It has been seen %pwned_count times before! Please use a stronger password...' forbidden_characters: '&4Your password contains illegal characters. Allowed chars: %valid_chars' wrong_length: '&cYour password is too short or too long! Please try with another one!' From b4f005189092c85a84b8233c81d6a22a13185e7a Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sat, 19 Nov 2022 09:48:45 +0100 Subject: [PATCH 4/4] Resolve checkstyle warnings --- src/main/java/fr/xephi/authme/message/MessageKey.java | 6 +++++- .../java/fr/xephi/authme/service/ValidationService.java | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/main/java/fr/xephi/authme/message/MessageKey.java b/src/main/java/fr/xephi/authme/message/MessageKey.java index 210cc64467..d201ac56aa 100644 --- a/src/main/java/fr/xephi/authme/message/MessageKey.java +++ b/src/main/java/fr/xephi/authme/message/MessageKey.java @@ -80,7 +80,11 @@ public enum MessageKey { /** The chosen password isn't safe, please choose another one... */ PASSWORD_UNSAFE_ERROR("password.unsafe_password"), - /** Your chosen password is not secure. It has been seen %pwned_count times before! Please use a stronger password... */ + /** + * Your chosen password is not secure. + * It has been seen %pwned_count times before! + * Please use a stronger password... + */ PASSWORD_PWNED_ERROR("password.pwned_password", "%pwned_count"), /** Your password contains illegal characters. Allowed chars: %valid_chars */ diff --git a/src/main/java/fr/xephi/authme/service/ValidationService.java b/src/main/java/fr/xephi/authme/service/ValidationService.java index 9f685e3129..4626d224a3 100644 --- a/src/main/java/fr/xephi/authme/service/ValidationService.java +++ b/src/main/java/fr/xephi/authme/service/ValidationService.java @@ -27,7 +27,6 @@ import java.io.DataInputStream; import java.net.HttpURLConnection; import java.net.URL; -import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Locale; @@ -258,8 +257,9 @@ public HaveIBeenPwnedResults validatePasswordHaveIBeenPwned(String password) { StringBuilder outStr = new StringBuilder(); try (DataInputStream input = new DataInputStream(connection.getInputStream())) { - for (int c = input.read(); c != -1; c = input.read()) + for (int c = input.read(); c != -1; c = input.read()) { outStr.append((char) c); + } } String[] hashes = outStr.toString().split("\n");