Skip to content

Add haveibeenpwned password check #2642

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

Open
wants to merge 4 commits 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
7 changes: 7 additions & 0 deletions src/main/java/fr/xephi/authme/message/MessageKey.java
Original file line number Diff line number Diff line change
Expand Up @@ -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"),

Expand Down
84 changes: 79 additions & 5 deletions src/main/java/fr/xephi/authme/service/ValidationService.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -35,7 +39,7 @@
* Validation service.
*/
public class ValidationService implements Reloadable {

private final ConsoleLogger logger = ConsoleLoggerFactory.get(ValidationService.class);

@Inject
Expand Down Expand Up @@ -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();
}

Expand All @@ -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)
*/
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -222,6 +235,49 @@ private Multimap<String, String> loadNameRestrictions(Set<String> 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;
Expand All @@ -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;
Expand All @@ -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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<Boolean> 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<Integer> 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<Boolean> TEMPBAN_ON_MAX_LOGINS =
newProperty("Security.tempban.enableTempban", false);
Expand Down
1 change: 1 addition & 0 deletions src/main/resources/messages/messages_en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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!'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down