#1188 Create and keep encryption method instance (#1191)

- Avoid creating the same object over and over; instead keep it and only change it on settings reload
This commit is contained in:
ljacqu 2017-04-30 17:41:51 +02:00 committed by Gabriele C
parent e0e4cd112d
commit c803822fa8
5 changed files with 35 additions and 48 deletions

View File

@ -1,7 +1,6 @@
package fr.xephi.authme.events; package fr.xephi.authme.events;
import fr.xephi.authme.security.crypts.EncryptionMethod; import fr.xephi.authme.security.crypts.EncryptionMethod;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList; import org.bukkit.event.HandlerList;
/** /**
@ -13,22 +12,19 @@ public class PasswordEncryptionEvent extends CustomEvent {
private static final HandlerList handlers = new HandlerList(); private static final HandlerList handlers = new HandlerList();
private EncryptionMethod method; private EncryptionMethod method;
private String playerName;
/** /**
* Constructor. * Constructor.
* *
* @param method The method used to encrypt the password * @param method The method used to encrypt the password
* @param playerName The name of the player
*/ */
public PasswordEncryptionEvent(EncryptionMethod method, String playerName) { public PasswordEncryptionEvent(EncryptionMethod method) {
super(false); super(false);
this.method = method; this.method = method;
this.playerName = playerName;
} }
/** /**
* Return the list of handlers, equivalent to {@link #getHandlers()} and required by {@link Event}. * Return the list of handlers, equivalent to {@link #getHandlers()} and required by {@link org.bukkit.event.Event}.
* *
* @return The list of handlers * @return The list of handlers
*/ */
@ -58,14 +54,4 @@ public class PasswordEncryptionEvent extends CustomEvent {
public void setMethod(EncryptionMethod method) { public void setMethod(EncryptionMethod method) {
this.method = method; this.method = method;
} }
/**
* Return the name of the player the event has been fired for.
*
* @return The player name
*/
public String getPlayerName() {
return playerName;
}
} }

View File

@ -29,9 +29,9 @@ public class PasswordSecurity implements Reloadable {
private PluginManager pluginManager; private PluginManager pluginManager;
@Inject @Inject
private Factory<EncryptionMethod> hashAlgorithmFactory; private Factory<EncryptionMethod> encryptionMethodFactory;
private HashAlgorithm algorithm; private EncryptionMethod encryptionMethod;
private Collection<HashAlgorithm> legacyAlgorithms; private Collection<HashAlgorithm> legacyAlgorithms;
/** /**
@ -40,7 +40,8 @@ public class PasswordSecurity implements Reloadable {
@PostConstruct @PostConstruct
@Override @Override
public void reload() { public void reload() {
this.algorithm = settings.getProperty(SecuritySettings.PASSWORD_HASH); HashAlgorithm algorithm = settings.getProperty(SecuritySettings.PASSWORD_HASH);
this.encryptionMethod = initializeEncryptionMethodWithEvent(algorithm);
this.legacyAlgorithms = settings.getProperty(SecuritySettings.LEGACY_HASHES); this.legacyAlgorithms = settings.getProperty(SecuritySettings.LEGACY_HASHES);
} }
@ -54,8 +55,7 @@ public class PasswordSecurity implements Reloadable {
*/ */
public HashedPassword computeHash(String password, String playerName) { public HashedPassword computeHash(String password, String playerName) {
String playerLowerCase = playerName.toLowerCase(); String playerLowerCase = playerName.toLowerCase();
EncryptionMethod method = initializeEncryptionMethodWithEvent(algorithm, playerLowerCase); return encryptionMethod.computeHash(password, playerLowerCase);
return method.computeHash(password, playerLowerCase);
} }
/** /**
@ -81,14 +81,13 @@ public class PasswordSecurity implements Reloadable {
* @return True if the password matches, false otherwise * @return True if the password matches, false otherwise
*/ */
public boolean comparePassword(String password, HashedPassword hashedPassword, String playerName) { public boolean comparePassword(String password, HashedPassword hashedPassword, String playerName) {
EncryptionMethod method = initializeEncryptionMethodWithEvent(algorithm, playerName);
String playerLowerCase = playerName.toLowerCase(); String playerLowerCase = playerName.toLowerCase();
return methodMatches(method, password, hashedPassword, playerLowerCase) return methodMatches(encryptionMethod, password, hashedPassword, playerLowerCase)
|| compareWithLegacyHashes(password, hashedPassword, playerLowerCase); || compareWithLegacyHashes(password, hashedPassword, playerLowerCase);
} }
/** /**
* Compare the given hash with all available encryption methods to support * Compare the given hash with the configured legacy encryption methods to support
* the migration to a new encryption method. Upon a successful match, the password * the migration to a new encryption method. Upon a successful match, the password
* will be hashed with the new encryption method and persisted. * will be hashed with the new encryption method and persisted.
* *
@ -96,13 +95,13 @@ public class PasswordSecurity implements Reloadable {
* @param hashedPassword The encrypted password to test the clear-text password against * @param hashedPassword The encrypted password to test the clear-text password against
* @param playerName The name of the player * @param playerName The name of the player
* *
* @return True if there was a password match with another encryption method, false otherwise * @return True if there was a password match with a configured legacy encryption method, false otherwise
*/ */
private boolean compareWithLegacyHashes(String password, HashedPassword hashedPassword, String playerName) { private boolean compareWithLegacyHashes(String password, HashedPassword hashedPassword, String playerName) {
for (HashAlgorithm algorithm : legacyAlgorithms) { for (HashAlgorithm algorithm : legacyAlgorithms) {
EncryptionMethod method = initializeEncryptionMethod(algorithm); EncryptionMethod method = initializeEncryptionMethod(algorithm);
if (methodMatches(method, password, hashedPassword, playerName)) { if (methodMatches(method, password, hashedPassword, playerName)) {
hashPasswordForNewAlgorithm(password, playerName); hashAndSavePasswordWithNewAlgorithm(password, playerName);
return true; return true;
} }
} }
@ -132,13 +131,12 @@ public class PasswordSecurity implements Reloadable {
* which may have been changed by an external listener. * which may have been changed by an external listener.
* *
* @param algorithm The algorithm to retrieve the encryption method for * @param algorithm The algorithm to retrieve the encryption method for
* @param playerName The name of the player a password will be hashed for
* *
* @return The encryption method * @return The encryption method
*/ */
private EncryptionMethod initializeEncryptionMethodWithEvent(HashAlgorithm algorithm, String playerName) { private EncryptionMethod initializeEncryptionMethodWithEvent(HashAlgorithm algorithm) {
EncryptionMethod method = initializeEncryptionMethod(algorithm); EncryptionMethod method = initializeEncryptionMethod(algorithm);
PasswordEncryptionEvent event = new PasswordEncryptionEvent(method, playerName); PasswordEncryptionEvent event = new PasswordEncryptionEvent(method);
pluginManager.callEvent(event); pluginManager.callEvent(event);
return event.getMethod(); return event.getMethod();
} }
@ -154,12 +152,11 @@ public class PasswordSecurity implements Reloadable {
if (HashAlgorithm.CUSTOM.equals(algorithm) || HashAlgorithm.PLAINTEXT.equals(algorithm)) { if (HashAlgorithm.CUSTOM.equals(algorithm) || HashAlgorithm.PLAINTEXT.equals(algorithm)) {
return null; return null;
} }
return hashAlgorithmFactory.newInstance(algorithm.getClazz()); return encryptionMethodFactory.newInstance(algorithm.getClazz());
} }
private void hashPasswordForNewAlgorithm(String password, String playerName) { private void hashAndSavePasswordWithNewAlgorithm(String password, String playerName) {
HashedPassword hashedPassword = initializeEncryptionMethodWithEvent(algorithm, playerName) HashedPassword hashedPassword = encryptionMethod.computeHash(password, playerName);
.computeHash(password, playerName);
dataSource.updatePassword(playerName, hashedPassword); dataSource.updatePassword(playerName, hashedPassword);
} }

View File

@ -61,10 +61,6 @@ public final class SecuritySettings implements SettingsHolder {
public static final Property<HashAlgorithm> PASSWORD_HASH = public static final Property<HashAlgorithm> PASSWORD_HASH =
newProperty(HashAlgorithm.class, "settings.security.passwordHash", HashAlgorithm.SHA256); newProperty(HashAlgorithm.class, "settings.security.passwordHash", HashAlgorithm.SHA256);
@Comment("Salt length for the SALTED2MD5 MD5(MD5(password)+salt)")
public static final Property<Integer> DOUBLE_MD5_SALT_LENGTH =
newProperty("settings.security.doubleMD5SaltLength", 8);
@Comment({ @Comment({
"If a password check fails, AuthMe will also try to check with the following hash methods.", "If a password check fails, AuthMe will also try to check with the following hash methods.",
"Use this setting when you change from one hash method to another.", "Use this setting when you change from one hash method to another.",
@ -75,6 +71,10 @@ public final class SecuritySettings implements SettingsHolder {
public static final Property<Set<HashAlgorithm>> LEGACY_HASHES = public static final Property<Set<HashAlgorithm>> LEGACY_HASHES =
new EnumSetProperty<>(HashAlgorithm.class, "settings.security.legacyHashes"); new EnumSetProperty<>(HashAlgorithm.class, "settings.security.legacyHashes");
@Comment("Salt length for the SALTED2MD5 MD5(MD5(password)+salt)")
public static final Property<Integer> DOUBLE_MD5_SALT_LENGTH =
newProperty("settings.security.doubleMD5SaltLength", 8);
@Comment("Number of rounds to use if passwordHash is set to PBKDF2. Default is 10000") @Comment("Number of rounds to use if passwordHash is set to PBKDF2. Default is 10000")
public static final Property<Integer> PBKDF2_NUMBER_OF_ROUNDS = public static final Property<Integer> PBKDF2_NUMBER_OF_ROUNDS =
newProperty("settings.security.pbkdf2Rounds", 10000); newProperty("settings.security.pbkdf2Rounds", 10000);

View File

@ -57,7 +57,10 @@ public class HashAlgorithmIntegrationTest {
// given / when / then // given / when / then
for (HashAlgorithm algorithm : HashAlgorithm.values()) { for (HashAlgorithm algorithm : HashAlgorithm.values()) {
if (!HashAlgorithm.CUSTOM.equals(algorithm) && !HashAlgorithm.PLAINTEXT.equals(algorithm)) { if (!HashAlgorithm.CUSTOM.equals(algorithm) && !HashAlgorithm.PLAINTEXT.equals(algorithm)) {
EncryptionMethod method = injector.newInstance(algorithm.getClazz()); EncryptionMethod method = injector.createIfHasDependencies(algorithm.getClazz());
if (method == null) {
fail("Could not create '" + algorithm.getClazz() + "' - forgot to provide some class?");
}
HashedPassword hashedPassword = method.computeHash("pwd", "name"); HashedPassword hashedPassword = method.computeHash("pwd", "name");
assertThat("Salt should not be null if method.hasSeparateSalt(), and vice versa. Method: '" assertThat("Salt should not be null if method.hasSeparateSalt(), and vice versa. Method: '"
+ method + "'", StringUtils.isEmpty(hashedPassword.getSalt()), equalTo(!method.hasSeparateSalt())); + method + "'", StringUtils.isEmpty(hashedPassword.getSalt()), equalTo(!method.hasSeparateSalt()));

View File

@ -13,6 +13,7 @@ import fr.xephi.authme.initialization.factory.Factory;
import fr.xephi.authme.security.crypts.EncryptionMethod; import fr.xephi.authme.security.crypts.EncryptionMethod;
import fr.xephi.authme.security.crypts.HashedPassword; import fr.xephi.authme.security.crypts.HashedPassword;
import fr.xephi.authme.security.crypts.Joomla; import fr.xephi.authme.security.crypts.Joomla;
import fr.xephi.authme.security.crypts.Md5;
import fr.xephi.authme.settings.Settings; import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.HooksSettings; import fr.xephi.authme.settings.properties.HooksSettings;
import fr.xephi.authme.settings.properties.SecuritySettings; import fr.xephi.authme.settings.properties.SecuritySettings;
@ -21,7 +22,6 @@ import org.bukkit.plugin.PluginManager;
import org.junit.BeforeClass; import org.junit.BeforeClass;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock; import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer; import org.mockito.stubbing.Answer;
@ -32,12 +32,14 @@ import java.util.Set;
import static com.google.common.collect.Sets.newHashSet; import static com.google.common.collect.Sets.newHashSet;
import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.equalToIgnoringCase; import static org.hamcrest.Matchers.equalToIgnoringCase;
import static org.hamcrest.Matchers.instanceOf;
import static org.junit.Assert.assertThat; import static org.junit.Assert.assertThat;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.never; import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.hamcrest.MockitoHamcrest.argThat; import static org.mockito.hamcrest.MockitoHamcrest.argThat;
@ -166,7 +168,6 @@ public class PasswordSecurityTest {
// then // then
assertThat(result, equalTo(false)); assertThat(result, equalTo(false));
verify(dataSource).getPassword(playerName); verify(dataSource).getPassword(playerName);
verify(pluginManager, never()).callEvent(any(Event.class));
verify(method, never()).comparePassword(anyString(), any(HashedPassword.class), anyString()); verify(method, never()).comparePassword(anyString(), any(HashedPassword.class), anyString());
} }
@ -204,7 +205,7 @@ public class PasswordSecurityTest {
} }
@Test @Test
public void shouldTryAllMethodsAndFail() { public void shouldTryLegacyMethodsAndFail() {
// given // given
HashedPassword password = new HashedPassword("hashNotMatchingAnyMethod", "someBogusSalt"); HashedPassword password = new HashedPassword("hashNotMatchingAnyMethod", "someBogusSalt");
String playerName = "asfd"; String playerName = "asfd";
@ -240,11 +241,9 @@ public class PasswordSecurityTest {
// then // then
assertThat(result, equalTo(hashedPassword)); assertThat(result, equalTo(hashedPassword));
ArgumentCaptor<PasswordEncryptionEvent> captor = ArgumentCaptor.forClass(PasswordEncryptionEvent.class); // Check that an event was fired twice: once on test setup, and once because we called reload()
verify(pluginManager).callEvent(captor.capture()); verify(pluginManager, times(2)).callEvent(any(PasswordEncryptionEvent.class));
PasswordEncryptionEvent event = captor.getValue();
assertThat(Joomla.class.equals(caughtClassInEvent), equalTo(true)); assertThat(Joomla.class.equals(caughtClassInEvent), equalTo(true));
assertThat(event.getPlayerName(), equalTo(usernameLowerCase));
} }
@Test @Test
@ -263,7 +262,8 @@ public class PasswordSecurityTest {
// then // then
assertThat(result, equalTo(false)); assertThat(result, equalTo(false));
verify(dataSource, never()).getAuth(anyString()); verify(dataSource, never()).getAuth(anyString());
verify(pluginManager).callEvent(any(PasswordEncryptionEvent.class)); // Check that an event was fired twice: once on test setup, and once because we called reload()
verify(pluginManager, times(2)).callEvent(any(PasswordEncryptionEvent.class));
verify(method, never()).comparePassword(anyString(), any(HashedPassword.class), anyString()); verify(method, never()).comparePassword(anyString(), any(HashedPassword.class), anyString());
} }
@ -273,13 +273,14 @@ public class PasswordSecurityTest {
given(settings.getProperty(SecuritySettings.PASSWORD_HASH)).willReturn(HashAlgorithm.MD5); given(settings.getProperty(SecuritySettings.PASSWORD_HASH)).willReturn(HashAlgorithm.MD5);
given(settings.getProperty(SecuritySettings.LEGACY_HASHES)) given(settings.getProperty(SecuritySettings.LEGACY_HASHES))
.willReturn(newHashSet(HashAlgorithm.CUSTOM, HashAlgorithm.BCRYPT)); .willReturn(newHashSet(HashAlgorithm.CUSTOM, HashAlgorithm.BCRYPT));
reset(pluginManager); // reset behavior when the event is emitted to check that we create an instance of Md5.java
// when // when
passwordSecurity.reload(); passwordSecurity.reload();
// then // then
assertThat(ReflectionTestUtils.getFieldValue(PasswordSecurity.class, passwordSecurity, "algorithm"), assertThat(ReflectionTestUtils.getFieldValue(PasswordSecurity.class, passwordSecurity, "encryptionMethod"),
equalTo(HashAlgorithm.MD5)); instanceOf(Md5.class));
Set<HashAlgorithm> legacyHashesSet = newHashSet(HashAlgorithm.CUSTOM, HashAlgorithm.BCRYPT); Set<HashAlgorithm> legacyHashesSet = newHashSet(HashAlgorithm.CUSTOM, HashAlgorithm.BCRYPT);
assertThat(ReflectionTestUtils.getFieldValue(PasswordSecurity.class, passwordSecurity, "legacyAlgorithms"), assertThat(ReflectionTestUtils.getFieldValue(PasswordSecurity.class, passwordSecurity, "legacyAlgorithms"),
equalTo(legacyHashesSet)); equalTo(legacyHashesSet));