diff --git a/src/main/java/fr/xephi/authme/message/MessageKey.java b/src/main/java/fr/xephi/authme/message/MessageKey.java index c7ed8dc35f..d201ac56aa 100644 --- a/src/main/java/fr/xephi/authme/message/MessageKey.java +++ b/src/main/java/fr/xephi/authme/message/MessageKey.java @@ -80,6 +80,13 @@ 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... + */ + 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..4626d224a3 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,9 @@ import javax.annotation.PostConstruct; import javax.inject.Inject; +import java.io.DataInputStream; +import java.net.HttpURLConnection; +import java.net.URL; import java.util.Collection; import java.util.List; import java.util.Locale; @@ -35,7 +39,7 @@ * Validation service. */ public class ValidationService implements Reloadable { - + private final ConsoleLogger logger = ConsoleLoggerFactory.get(ValidationService.class); @Inject @@ -80,7 +84,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 +116,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 +191,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 +235,49 @@ 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.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"); + for (String hashSuffix : hashes) { + String[] hashSuffixParts = hashSuffix.trim().split(":"); + if (hashSuffixParts[0].equalsIgnoreCase(hash.substring(5))) { + 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 +294,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 +318,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..02bd1b26f2 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 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!' 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);