295 lines
12 KiB
Java
295 lines
12 KiB
Java
package fr.xephi.authme.security;
|
|
|
|
import ch.jalu.injector.Injector;
|
|
import ch.jalu.injector.InjectorBuilder;
|
|
import fr.xephi.authme.ReflectionTestUtils;
|
|
import fr.xephi.authme.TestHelper;
|
|
import fr.xephi.authme.datasource.DataSource;
|
|
import fr.xephi.authme.events.PasswordEncryptionEvent;
|
|
import fr.xephi.authme.initialization.factory.FactoryDependencyHandler;
|
|
import fr.xephi.authme.security.crypts.EncryptionMethod;
|
|
import fr.xephi.authme.security.crypts.HashedPassword;
|
|
import fr.xephi.authme.security.crypts.Joomla;
|
|
import fr.xephi.authme.settings.Settings;
|
|
import fr.xephi.authme.settings.properties.HooksSettings;
|
|
import fr.xephi.authme.settings.properties.SecuritySettings;
|
|
import org.bukkit.event.Event;
|
|
import org.bukkit.plugin.PluginManager;
|
|
import org.junit.Before;
|
|
import org.junit.BeforeClass;
|
|
import org.junit.Test;
|
|
import org.junit.runner.RunWith;
|
|
import org.mockito.ArgumentCaptor;
|
|
import org.mockito.Mock;
|
|
import org.mockito.invocation.InvocationOnMock;
|
|
import org.mockito.junit.MockitoJUnitRunner;
|
|
import org.mockito.stubbing.Answer;
|
|
|
|
import java.util.Collections;
|
|
import java.util.Set;
|
|
|
|
import static com.google.common.collect.Sets.newHashSet;
|
|
import static org.hamcrest.Matchers.equalTo;
|
|
import static org.hamcrest.Matchers.equalToIgnoringCase;
|
|
import static org.junit.Assert.assertThat;
|
|
import static org.mockito.ArgumentMatchers.any;
|
|
import static org.mockito.ArgumentMatchers.anyString;
|
|
import static org.mockito.BDDMockito.given;
|
|
import static org.mockito.Mockito.doAnswer;
|
|
import static org.mockito.Mockito.never;
|
|
import static org.mockito.Mockito.times;
|
|
import static org.mockito.Mockito.verify;
|
|
import static org.mockito.hamcrest.MockitoHamcrest.argThat;
|
|
|
|
/**
|
|
* Test for {@link PasswordSecurity}.
|
|
*/
|
|
@RunWith(MockitoJUnitRunner.class)
|
|
public class PasswordSecurityTest {
|
|
|
|
private Injector injector;
|
|
|
|
@Mock
|
|
private Settings settings;
|
|
|
|
@Mock
|
|
private PluginManager pluginManager;
|
|
|
|
@Mock
|
|
private DataSource dataSource;
|
|
|
|
@Mock
|
|
private EncryptionMethod method;
|
|
|
|
private Class<?> caughtClassInEvent;
|
|
|
|
@BeforeClass
|
|
public static void setUpTest() {
|
|
TestHelper.setupLogger();
|
|
}
|
|
|
|
@Before
|
|
public void setUpMocks() {
|
|
caughtClassInEvent = null;
|
|
|
|
// When the password encryption event is emitted, replace the encryption method with our mock.
|
|
doAnswer(new Answer<Void>() {
|
|
@Override
|
|
public Void answer(InvocationOnMock invocation) throws Throwable {
|
|
Object[] arguments = invocation.getArguments();
|
|
if (arguments[0] instanceof PasswordEncryptionEvent) {
|
|
PasswordEncryptionEvent event = (PasswordEncryptionEvent) arguments[0];
|
|
caughtClassInEvent = event.getMethod() == null ? null : event.getMethod().getClass();
|
|
event.setMethod(method);
|
|
}
|
|
return null;
|
|
}
|
|
}).when(pluginManager).callEvent(any(Event.class));
|
|
injector = new InjectorBuilder()
|
|
.addHandlers(new FactoryDependencyHandler())
|
|
.addDefaultHandlers("fr.xephi.authme").create();
|
|
injector.register(Settings.class, settings);
|
|
injector.register(DataSource.class, dataSource);
|
|
injector.register(PluginManager.class, pluginManager);
|
|
}
|
|
|
|
@Test
|
|
public void shouldReturnPasswordMatch() {
|
|
// given
|
|
HashedPassword password = new HashedPassword("$TEST$10$SOME_HASH", null);
|
|
String playerName = "Tester";
|
|
// Calls to EncryptionMethod are always with the lower-case version of the name
|
|
String playerLowerCase = playerName.toLowerCase();
|
|
String clearTextPass = "myPassTest";
|
|
|
|
given(dataSource.getPassword(playerName)).willReturn(password);
|
|
given(method.comparePassword(clearTextPass, password, playerLowerCase)).willReturn(true);
|
|
initSettings(HashAlgorithm.BCRYPT);
|
|
PasswordSecurity security = newPasswordSecurity();
|
|
|
|
// when
|
|
boolean result = security.comparePassword(clearTextPass, playerName);
|
|
|
|
// then
|
|
assertThat(result, equalTo(true));
|
|
verify(dataSource).getPassword(playerName);
|
|
verify(pluginManager).callEvent(any(PasswordEncryptionEvent.class));
|
|
verify(method).comparePassword(clearTextPass, password, playerLowerCase);
|
|
}
|
|
|
|
@Test
|
|
public void shouldReturnPasswordMismatch() {
|
|
// given
|
|
HashedPassword password = new HashedPassword("$TEST$10$SOME_HASH", null);
|
|
String playerName = "My_PLayer";
|
|
String playerLowerCase = playerName.toLowerCase();
|
|
String clearTextPass = "passw0Rd1";
|
|
|
|
given(dataSource.getPassword(playerName)).willReturn(password);
|
|
given(method.comparePassword(clearTextPass, password, playerLowerCase)).willReturn(false);
|
|
initSettings(HashAlgorithm.CUSTOM);
|
|
PasswordSecurity security = newPasswordSecurity();
|
|
|
|
// when
|
|
boolean result = security.comparePassword(clearTextPass, playerName);
|
|
|
|
// then
|
|
assertThat(result, equalTo(false));
|
|
verify(dataSource).getPassword(playerName);
|
|
verify(pluginManager).callEvent(any(PasswordEncryptionEvent.class));
|
|
verify(method).comparePassword(clearTextPass, password, playerLowerCase);
|
|
}
|
|
|
|
@Test
|
|
public void shouldReturnFalseIfPlayerDoesNotExist() {
|
|
// given
|
|
String playerName = "bobby";
|
|
String clearTextPass = "tables";
|
|
|
|
given(dataSource.getPassword(playerName)).willReturn(null);
|
|
initSettings(HashAlgorithm.MD5);
|
|
PasswordSecurity security = newPasswordSecurity();
|
|
|
|
// when
|
|
boolean result = security.comparePassword(clearTextPass, playerName);
|
|
|
|
// then
|
|
assertThat(result, equalTo(false));
|
|
verify(dataSource).getPassword(playerName);
|
|
verify(pluginManager, never()).callEvent(any(Event.class));
|
|
verify(method, never()).comparePassword(anyString(), any(HashedPassword.class), anyString());
|
|
}
|
|
|
|
@Test
|
|
public void shouldTryOtherMethodsForFailedPassword() {
|
|
// given
|
|
// BCRYPT hash for "Test"
|
|
HashedPassword password =
|
|
new HashedPassword("$2y$10$2e6d2193f43501c926e25elvWlPmWczmrfrnbZV0dUZGITjYjnkkW");
|
|
String playerName = "somePlayer";
|
|
String playerLowerCase = playerName.toLowerCase();
|
|
String clearTextPass = "Test";
|
|
// MD5 hash for "Test"
|
|
HashedPassword newPassword = new HashedPassword("0cbc6611f5540bd0809a388dc95a615b");
|
|
|
|
given(dataSource.getPassword(argThat(equalToIgnoringCase(playerName)))).willReturn(password);
|
|
given(method.comparePassword(clearTextPass, password, playerLowerCase)).willReturn(false);
|
|
given(method.computeHash(clearTextPass, playerLowerCase)).willReturn(newPassword);
|
|
initSettings(HashAlgorithm.MD5);
|
|
given(settings.getProperty(SecuritySettings.LEGACY_HASHES)).willReturn(newHashSet(HashAlgorithm.BCRYPT));
|
|
PasswordSecurity security = newPasswordSecurity();
|
|
|
|
// when
|
|
boolean result = security.comparePassword(clearTextPass, playerName);
|
|
|
|
// then
|
|
assertThat(result, equalTo(true));
|
|
// Note ljacqu 20151230: We need to check the player name in a case-insensitive way because the methods within
|
|
// PasswordSecurity may convert the name into all lower-case. This is desired because EncryptionMethod methods
|
|
// should only be invoked with all lower-case names. Data source is case-insensitive itself, so this is fine.
|
|
verify(dataSource).getPassword(argThat(equalToIgnoringCase(playerName)));
|
|
verify(pluginManager, times(2)).callEvent(any(PasswordEncryptionEvent.class));
|
|
verify(method).comparePassword(clearTextPass, password, playerLowerCase);
|
|
verify(dataSource).updatePassword(playerLowerCase, newPassword);
|
|
}
|
|
|
|
@Test
|
|
public void shouldTryAllMethodsAndFail() {
|
|
// given
|
|
HashedPassword password = new HashedPassword("hashNotMatchingAnyMethod", "someBogusSalt");
|
|
String playerName = "asfd";
|
|
String clearTextPass = "someInvalidPassword";
|
|
given(dataSource.getPassword(playerName)).willReturn(password);
|
|
given(method.comparePassword(clearTextPass, password, playerName)).willReturn(false);
|
|
initSettings(HashAlgorithm.MD5);
|
|
PasswordSecurity security = newPasswordSecurity();
|
|
|
|
// when
|
|
boolean result = security.comparePassword(clearTextPass, playerName);
|
|
|
|
// then
|
|
assertThat(result, equalTo(false));
|
|
verify(dataSource, never()).updatePassword(anyString(), any(HashedPassword.class));
|
|
}
|
|
|
|
@Test
|
|
public void shouldHashPassword() {
|
|
// given
|
|
String password = "MyP@ssword";
|
|
String username = "theUserInTest";
|
|
String usernameLowerCase = username.toLowerCase();
|
|
HashedPassword hashedPassword = new HashedPassword("$T$est#Hash", "__someSalt__");
|
|
given(method.computeHash(password, usernameLowerCase)).willReturn(hashedPassword);
|
|
initSettings(HashAlgorithm.JOOMLA);
|
|
PasswordSecurity security = newPasswordSecurity();
|
|
|
|
// when
|
|
HashedPassword result = security.computeHash(password, username);
|
|
|
|
// then
|
|
assertThat(result, equalTo(hashedPassword));
|
|
ArgumentCaptor<PasswordEncryptionEvent> captor = ArgumentCaptor.forClass(PasswordEncryptionEvent.class);
|
|
verify(pluginManager).callEvent(captor.capture());
|
|
PasswordEncryptionEvent event = captor.getValue();
|
|
assertThat(Joomla.class.equals(caughtClassInEvent), equalTo(true));
|
|
assertThat(event.getPlayerName(), equalTo(usernameLowerCase));
|
|
}
|
|
|
|
@Test
|
|
public void shouldSkipCheckIfMandatorySaltIsUnavailable() {
|
|
// given
|
|
String password = "?topSecretPass\\";
|
|
String username = "someone12";
|
|
HashedPassword hashedPassword = new HashedPassword("~T!est#Hash");
|
|
given(method.hasSeparateSalt()).willReturn(true);
|
|
initSettings(HashAlgorithm.XAUTH);
|
|
PasswordSecurity security = newPasswordSecurity();
|
|
|
|
// when
|
|
boolean result = security.comparePassword(password, hashedPassword, username);
|
|
|
|
// then
|
|
assertThat(result, equalTo(false));
|
|
verify(dataSource, never()).getAuth(anyString());
|
|
verify(pluginManager).callEvent(any(PasswordEncryptionEvent.class));
|
|
verify(method, never()).comparePassword(anyString(), any(HashedPassword.class), anyString());
|
|
}
|
|
|
|
@Test
|
|
public void shouldReloadSettings() {
|
|
// given
|
|
initSettings(HashAlgorithm.BCRYPT);
|
|
PasswordSecurity passwordSecurity = newPasswordSecurity();
|
|
given(settings.getProperty(SecuritySettings.PASSWORD_HASH)).willReturn(HashAlgorithm.MD5);
|
|
given(settings.getProperty(SecuritySettings.LEGACY_HASHES))
|
|
.willReturn(newHashSet(HashAlgorithm.CUSTOM, HashAlgorithm.BCRYPT));
|
|
|
|
// when
|
|
passwordSecurity.reload();
|
|
|
|
// then
|
|
assertThat(ReflectionTestUtils.getFieldValue(PasswordSecurity.class, passwordSecurity, "algorithm"),
|
|
equalTo(HashAlgorithm.MD5));
|
|
Set<HashAlgorithm> legacyHashesSet = newHashSet(HashAlgorithm.CUSTOM, HashAlgorithm.BCRYPT);
|
|
assertThat(ReflectionTestUtils.getFieldValue(PasswordSecurity.class, passwordSecurity, "legacyAlgorithms"),
|
|
equalTo(legacyHashesSet));
|
|
}
|
|
|
|
private PasswordSecurity newPasswordSecurity() {
|
|
// Use this method to make sure we have all dependents of PasswordSecurity already registered as mocks
|
|
PasswordSecurity passwordSecurity = injector.createIfHasDependencies(PasswordSecurity.class);
|
|
if (passwordSecurity == null) {
|
|
throw new IllegalStateException("Cannot create PasswordSecurity directly! "
|
|
+ "Did you forget to provide a dependency as mock?");
|
|
}
|
|
return passwordSecurity;
|
|
}
|
|
|
|
private void initSettings(HashAlgorithm algorithm) {
|
|
given(settings.getProperty(SecuritySettings.PASSWORD_HASH)).willReturn(algorithm);
|
|
given(settings.getProperty(SecuritySettings.LEGACY_HASHES)).willReturn(Collections.emptySet());
|
|
given(settings.getProperty(HooksSettings.BCRYPT_LOG2_ROUND)).willReturn(8);
|
|
}
|
|
|
|
}
|