diff --git a/src/main/java/fr/xephi/authme/security/HashAlgorithm.java b/src/main/java/fr/xephi/authme/security/HashAlgorithm.java index 2cfc1b4b..732582e0 100644 --- a/src/main/java/fr/xephi/authme/security/HashAlgorithm.java +++ b/src/main/java/fr/xephi/authme/security/HashAlgorithm.java @@ -1,8 +1,6 @@ package fr.xephi.authme.security; import fr.xephi.authme.security.crypts.EncryptionMethod; -import fr.xephi.authme.security.crypts.Pbkdf2; -import fr.xephi.authme.security.crypts.Pbkdf2Django; /** * Hash algorithms supported by AuthMe. @@ -19,8 +17,8 @@ public enum HashAlgorithm { MD5(fr.xephi.authme.security.crypts.MD5.class), MD5VB(fr.xephi.authme.security.crypts.MD5VB.class), MYBB(fr.xephi.authme.security.crypts.MYBB.class), - PBKDF2(Pbkdf2.class), - PBKDF2DJANGO(Pbkdf2Django.class), + PBKDF2(fr.xephi.authme.security.crypts.Pbkdf2.class), + PBKDF2DJANGO(fr.xephi.authme.security.crypts.Pbkdf2Django.class), PHPBB(fr.xephi.authme.security.crypts.PHPBB.class), PHPFUSION(fr.xephi.authme.security.crypts.PHPFUSION.class), @Deprecated diff --git a/src/main/java/fr/xephi/authme/security/crypts/Pbkdf2.java b/src/main/java/fr/xephi/authme/security/crypts/Pbkdf2.java index 6b4dd625..5367a2a1 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/Pbkdf2.java +++ b/src/main/java/fr/xephi/authme/security/crypts/Pbkdf2.java @@ -3,18 +3,30 @@ package fr.xephi.authme.security.crypts; import de.rtner.misc.BinTools; import de.rtner.security.auth.spi.PBKDF2Engine; import de.rtner.security.auth.spi.PBKDF2Parameters; +import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.security.crypts.description.Recommendation; import fr.xephi.authme.security.crypts.description.Usage; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.SecuritySettings; + +import javax.inject.Inject; @Recommendation(Usage.RECOMMENDED) public class Pbkdf2 extends HexSaltedMethod { - private static final int NUMBER_OF_ITERATIONS = 10_000; + private static final int DEFAULT_ROUNDS = 10_000; + private int numberOfRounds; + + @Inject + Pbkdf2(Settings settings) { + int configuredRounds = settings.getProperty(SecuritySettings.PBKDF2_NUMBER_OF_ROUNDS); + this.numberOfRounds = configuredRounds > 0 ? configuredRounds : DEFAULT_ROUNDS; + } @Override public String computeHash(String password, String salt, String name) { - String result = "pbkdf2_sha256$" + NUMBER_OF_ITERATIONS + "$" + salt + "$"; - PBKDF2Parameters params = new PBKDF2Parameters("HmacSHA256", "UTF-8", salt.getBytes(), NUMBER_OF_ITERATIONS); + String result = "pbkdf2_sha256$" + numberOfRounds + "$" + salt + "$"; + PBKDF2Parameters params = new PBKDF2Parameters("HmacSHA256", "UTF-8", salt.getBytes(), numberOfRounds); PBKDF2Engine engine = new PBKDF2Engine(params); return result + BinTools.bin2hex(engine.deriveKey(password, 64)); @@ -26,9 +38,16 @@ public class Pbkdf2 extends HexSaltedMethod { if (line.length != 4) { return false; } + int iterations; + try { + iterations = Integer.parseInt(line[1]); + } catch (NumberFormatException e) { + ConsoleLogger.logException("Cannot read number of rounds for Pbkdf2", e); + return false; + } String salt = line[2]; byte[] derivedKey = BinTools.hex2bin(line[3]); - PBKDF2Parameters params = new PBKDF2Parameters("HmacSHA256", "UTF-8", salt.getBytes(), 10000, derivedKey); + PBKDF2Parameters params = new PBKDF2Parameters("HmacSHA256", "UTF-8", salt.getBytes(), iterations, derivedKey); PBKDF2Engine engine = new PBKDF2Engine(params); return engine.verifyKey(password); } diff --git a/src/main/java/fr/xephi/authme/security/crypts/Pbkdf2Django.java b/src/main/java/fr/xephi/authme/security/crypts/Pbkdf2Django.java index 528c1eb1..93988e0d 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/Pbkdf2Django.java +++ b/src/main/java/fr/xephi/authme/security/crypts/Pbkdf2Django.java @@ -4,7 +4,6 @@ import de.rtner.security.auth.spi.PBKDF2Engine; import de.rtner.security.auth.spi.PBKDF2Parameters; import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.security.crypts.description.AsciiRestricted; -import fr.xephi.authme.util.StringUtils; import javax.xml.bind.DatatypeConverter; @@ -32,8 +31,7 @@ public class Pbkdf2Django extends HexSaltedMethod { try { iterations = Integer.parseInt(line[1]); } catch (NumberFormatException e) { - ConsoleLogger.warning("Could not read number of rounds for Pbkdf2Django:" - + StringUtils.formatException(e)); + ConsoleLogger.logException("Could not read number of rounds for Pbkdf2Django:", e); return false; } String salt = line[2]; 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 80b0fdf1..5bad44b0 100644 --- a/src/main/java/fr/xephi/authme/settings/properties/SecuritySettings.java +++ b/src/main/java/fr/xephi/authme/settings/properties/SecuritySettings.java @@ -86,6 +86,10 @@ public class SecuritySettings implements SettingsHolder { public static final Property> LEGACY_HASHES = new EnumSetProperty<>(HashAlgorithm.class, "settings.security.legacyHashes"); + @Comment("Number of rounds to use if passwordHash is set to PBKDF2. Default is 10000") + public static final Property PBKDF2_NUMBER_OF_ROUNDS = + newProperty("settings.security.pbkdf2Rounds", 10000); + @Comment({"Prevent unsafe passwords from being used; put them in lowercase!", "You should always set 'help' as unsafePassword due to possible conflicts.", "unsafePasswords:", diff --git a/src/test/java/fr/xephi/authme/security/HashAlgorithmIntegrationTest.java b/src/test/java/fr/xephi/authme/security/HashAlgorithmIntegrationTest.java index fcfa4fad..2d536819 100644 --- a/src/test/java/fr/xephi/authme/security/HashAlgorithmIntegrationTest.java +++ b/src/test/java/fr/xephi/authme/security/HashAlgorithmIntegrationTest.java @@ -32,6 +32,7 @@ public class HashAlgorithmIntegrationTest { Settings settings = mock(Settings.class); given(settings.getProperty(HooksSettings.BCRYPT_LOG2_ROUND)).willReturn(8); given(settings.getProperty(SecuritySettings.DOUBLE_MD5_SALT_LENGTH)).willReturn(16); + given(settings.getProperty(SecuritySettings.PBKDF2_NUMBER_OF_ROUNDS)).willReturn(10_000); injector = new InjectorBuilder().addDefaultHandlers("fr.xephi.authme").create(); injector.register(Settings.class, settings); } diff --git a/src/test/java/fr/xephi/authme/security/crypts/Pbkdf2Test.java b/src/test/java/fr/xephi/authme/security/crypts/Pbkdf2Test.java index 1d7b8de7..8296e56e 100644 --- a/src/test/java/fr/xephi/authme/security/crypts/Pbkdf2Test.java +++ b/src/test/java/fr/xephi/authme/security/crypts/Pbkdf2Test.java @@ -1,16 +1,44 @@ package fr.xephi.authme.security.crypts; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.SecuritySettings; +import org.junit.Test; + +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + /** * Test for {@link Pbkdf2}. */ public class Pbkdf2Test extends AbstractEncryptionMethodTest { public Pbkdf2Test() { - super(new Pbkdf2(), + super(new Pbkdf2(mockSettings()), "pbkdf2_sha256$10000$b25801311edf$093E38B16DFF13FCE5CD64D5D888EE6E0376A3E572FE5DA6749515EA0F384413223A21C464B0BE899E64084D1FFEFD44F2AC768453C87F41B42CC6954C416900", // password "pbkdf2_sha256$10000$fe705da06c57$A41527BD58FED9C9E6F452FC1BA8B0C4C4224ECC63E37F71EB1A0865D2AB81BBFEBCA9B7B6A6E8AEF4717B43F8EB6FB4EDEFFBB399D9D991EF7E23013595BAF0", // PassWord1 "pbkdf2_sha256$10000$05603593cdda$1D30D1D90D826C866755969F06C312E21CC3E8DA0B777E2C764700E4E1FD890B731FAF44753D68F3FC025D3EAA709E800FBF2AF61DB23464311FCE7D35353A30", // &^%te$t?Pw@_ "pbkdf2_sha256$10000$fb944d66d754$F7E3BF8CB07CE3B3C8C5C534F803252F7B4FD58832E33BA62BA46CA06F23BAE12BE03A9CB5874BCFD4469E42972406F920E59F002247B23C22A8CF3D0E7BFFE0"); // âË_3(íù* } + @Test + public void shouldDetectMatchForHashWithOtherRoundNumber() { + // given + Pbkdf2 pbkdf2 = new Pbkdf2(mockSettings()); + String hash = "pbkdf2_sha256$4128$3469b0d48b702046$DC8A54351008C6054E12FB19E0BF8A4EA6D4165E0EDC97A1ECD15231037C382DE5BF85D07D5BC9D1ADF9BBFE4CE257C6059FB1B9FF65DB69D8B205F064BE0DA9"; + String clearText = "PassWord1"; + + // when + boolean isMatch = pbkdf2.comparePassword(clearText, new HashedPassword(hash), ""); + + // then + assertThat(isMatch, equalTo(true)); + } + + private static Settings mockSettings() { + Settings settings = mock(Settings.class); + given(settings.getProperty(SecuritySettings.PBKDF2_NUMBER_OF_ROUNDS)).willReturn(4128); + return settings; + } }