Skip to content

Commit 6fd0df6

Browse files
Add haveibeenpwned check
1 parent a14df80 commit 6fd0df6

5 files changed

Lines changed: 99 additions & 5 deletions

File tree

src/main/java/fr/xephi/authme/message/MessageKey.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@ public enum MessageKey {
8080
/** The chosen password isn't safe, please choose another one... */
8181
PASSWORD_UNSAFE_ERROR("password.unsafe_password"),
8282

83+
/** Your chosen password has been used %pwned_count times worldwide! Please use a strong password... */
84+
PASSWORD_PWNED_ERROR("password.pwned_password", "%pwned_count"),
85+
8386
/** Your password contains illegal characters. Allowed chars: %valid_chars */
8487
PASSWORD_CHARACTERS_ERROR("password.forbidden_characters", "%valid_chars"),
8588

src/main/java/fr/xephi/authme/service/ValidationService.java

Lines changed: 84 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@
66
import fr.xephi.authme.ConsoleLogger;
77
import fr.xephi.authme.datasource.DataSource;
88
import fr.xephi.authme.initialization.Reloadable;
9-
import fr.xephi.authme.output.ConsoleLoggerFactory;
109
import fr.xephi.authme.message.MessageKey;
10+
import fr.xephi.authme.output.ConsoleLoggerFactory;
1111
import fr.xephi.authme.permission.PermissionsManager;
1212
import fr.xephi.authme.permission.PlayerStatePermission;
13+
import fr.xephi.authme.security.HashUtils;
1314
import fr.xephi.authme.settings.Settings;
1415
import fr.xephi.authme.settings.properties.EmailSettings;
1516
import fr.xephi.authme.settings.properties.ProtectionSettings;
@@ -23,6 +24,10 @@
2324

2425
import javax.annotation.PostConstruct;
2526
import javax.inject.Inject;
27+
import java.io.DataInputStream;
28+
import java.net.HttpURLConnection;
29+
import java.net.URL;
30+
import java.util.Arrays;
2631
import java.util.Collection;
2732
import java.util.List;
2833
import java.util.Locale;
@@ -35,7 +40,7 @@
3540
* Validation service.
3641
*/
3742
public class ValidationService implements Reloadable {
38-
43+
3944
private final ConsoleLogger logger = ConsoleLoggerFactory.get(ValidationService.class);
4045

4146
@Inject
@@ -80,7 +85,16 @@ public ValidationResult validatePassword(String password, String username) {
8085
return new ValidationResult(MessageKey.INVALID_PASSWORD_LENGTH);
8186
} else if (settings.getProperty(SecuritySettings.UNSAFE_PASSWORDS).contains(passLow)) {
8287
return new ValidationResult(MessageKey.PASSWORD_UNSAFE_ERROR);
88+
} else if (settings.getProperty(SecuritySettings.HAVE_I_BEEN_PWNED_CHECK)) {
89+
HaveIBeenPwnedResults results = validatePasswordHaveIBeenPwned(password);
90+
91+
if (results != null
92+
&& results.isPwned()
93+
&& results.getPwnCount() > settings.getProperty(SecuritySettings.HAVE_I_BEEN_PWNED_LIMIT)) {
94+
return new ValidationResult(MessageKey.PASSWORD_PWNED_ERROR, String.valueOf(results.getPwnCount()));
95+
}
8396
}
97+
8498
return new ValidationResult();
8599
}
86100

@@ -103,7 +117,7 @@ public boolean validateEmail(String email) {
103117
* Queries the database whether the email is still free for registration, i.e. whether the given
104118
* command sender may use the email to register a new account (as defined by settings and permissions).
105119
*
106-
* @param email the email to verify
120+
* @param email the email to verify
107121
* @param sender the command sender
108122
* @return true if the email may be used, false otherwise (registration threshold has been exceeded)
109123
*/
@@ -178,7 +192,7 @@ public boolean fulfillsNameRestrictions(Player player) {
178192
* Whitelist has precedence over blacklist: if a whitelist is set, the value is rejected if not present
179193
* in the whitelist.
180194
*
181-
* @param value the value to verify
195+
* @param value the value to verify
182196
* @param whitelist the whitelist property
183197
* @param blacklist the blacklist property
184198
* @return true if the value is admitted by the lists, false otherwise
@@ -222,6 +236,53 @@ private Multimap<String, String> loadNameRestrictions(Set<String> configuredRest
222236
return restrictions;
223237
}
224238

239+
/**
240+
* Check haveibeenpwned.com for the given password.
241+
*
242+
* @param password password to check for
243+
* @return Results of the check
244+
*/
245+
public HaveIBeenPwnedResults validatePasswordHaveIBeenPwned(String password) {
246+
String hash = HashUtils.sha1(password);
247+
248+
String hashPrefix = hash.substring(0, 5);
249+
250+
try {
251+
String url = String.format("https://api.pwnedpasswords.com/range/%s", hashPrefix);
252+
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
253+
connection.setRequestMethod("GET");
254+
connection.setRequestProperty("User-Agent", "AuthMeReloaded");
255+
connection.setConnectTimeout(5000);
256+
connection.setReadTimeout(5000);
257+
connection.setDoOutput(true);
258+
connection.setDoInput(true);
259+
StringBuilder outStr = new StringBuilder();
260+
261+
try (DataInputStream input = new DataInputStream(connection.getInputStream())) {
262+
for (int c = input.read(); c != -1; c = input.read())
263+
outStr.append((char) c);
264+
}
265+
266+
String[] hashes = outStr.toString().split("\n");
267+
System.out.println(Arrays.toString(hashes));
268+
for (String hashSuffix : hashes) {
269+
String[] hashSuffixParts = hashSuffix.trim().split(":");
270+
System.out.println(Arrays.toString(hashSuffixParts));
271+
if (hashSuffixParts[0].equalsIgnoreCase(hash.substring(5))) {
272+
System.out.println("Found match");
273+
System.out.println(hashSuffixParts[1]);
274+
return new HaveIBeenPwnedResults(true, Integer.parseInt(hashSuffixParts[1]));
275+
}
276+
}
277+
278+
return new HaveIBeenPwnedResults(false, 0);
279+
} catch (Exception e) {
280+
e.printStackTrace();
281+
}
282+
283+
return null;
284+
}
285+
225286
public static final class ValidationResult {
226287
private final MessageKey messageKey;
227288
private final String[] args;
@@ -238,7 +299,7 @@ public ValidationResult() {
238299
* Constructor for a failed validation.
239300
*
240301
* @param messageKey message key of the validation error
241-
* @param args arguments for the message key
302+
* @param args arguments for the message key
242303
*/
243304
public ValidationResult(MessageKey messageKey, String... args) {
244305
this.messageKey = messageKey;
@@ -262,4 +323,22 @@ public String[] getArgs() {
262323
return args;
263324
}
264325
}
326+
327+
public static final class HaveIBeenPwnedResults {
328+
private final boolean isPwned;
329+
private final int pwnCount;
330+
331+
public HaveIBeenPwnedResults(boolean isPwned, int pwnCount) {
332+
this.isPwned = isPwned;
333+
this.pwnCount = pwnCount;
334+
}
335+
336+
public boolean isPwned() {
337+
return isPwned;
338+
}
339+
340+
public int getPwnCount() {
341+
return pwnCount;
342+
}
343+
}
265344
}

src/main/java/fr/xephi/authme/settings/properties/SecuritySettings.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,15 @@ public final class SecuritySettings implements SettingsHolder {
8989
newLowercaseStringSetProperty("settings.security.unsafePasswords",
9090
"123456", "password", "qwerty", "12345", "54321", "123456789", "help");
9191

92+
@Comment({"Query haveibeenpwned.com with a hashed version of the password.",
93+
"This is used to check whether it is safe."})
94+
public static final Property<Boolean> HAVE_I_BEEN_PWNED_CHECK =
95+
newProperty("settings.security.haveIBeenPwned.check", true);
96+
97+
@Comment({"If the password is used more than this number of times, it is considered unsafe."})
98+
public static final Property<Integer> HAVE_I_BEEN_PWNED_LIMIT =
99+
newProperty("settings.security.haveIBeenPwned.limit", 0);
100+
92101
@Comment("Tempban a user's IP address if they enter the wrong password too many times")
93102
public static final Property<Boolean> TEMPBAN_ON_MAX_LOGINS =
94103
newProperty("Security.tempban.enableTempban", false);

src/main/resources/messages/messages_en.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ password:
1818
match_error: '&cPasswords didn''t match, check them again!'
1919
name_in_password: '&cYou can''t use your name as password, please choose another one...'
2020
unsafe_password: '&cThe chosen password isn''t safe, please choose another one...'
21+
pwned_password: '&cYour chosen password is not secure. It was used %pwned_count times already! Please use a strong password...'
2122
forbidden_characters: '&4Your password contains illegal characters. Allowed chars: %valid_chars'
2223
wrong_length: '&cYour password is too short or too long! Please try with another one!'
2324

src/test/java/fr/xephi/authme/service/ValidationServiceTest.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ public void createService() {
5858
given(settings.getProperty(SecuritySettings.MIN_PASSWORD_LENGTH)).willReturn(3);
5959
given(settings.getProperty(SecuritySettings.MAX_PASSWORD_LENGTH)).willReturn(20);
6060
given(settings.getProperty(SecuritySettings.UNSAFE_PASSWORDS)).willReturn(newHashSet("unsafe", "other-unsafe"));
61+
given(settings.getProperty(SecuritySettings.HAVE_I_BEEN_PWNED_CHECK)).willReturn(true);
62+
given(settings.getProperty(SecuritySettings.HAVE_I_BEEN_PWNED_LIMIT)).willReturn(0);
6163
given(settings.getProperty(EmailSettings.MAX_REG_PER_EMAIL)).willReturn(3);
6264
given(settings.getProperty(RestrictionSettings.UNRESTRICTED_NAMES)).willReturn(newHashSet("name01", "npc"));
6365
given(settings.getProperty(RestrictionSettings.ENABLE_RESTRICTED_USERS)).willReturn(false);

0 commit comments

Comments
 (0)