LoginSystem/src/test/java/fr/xephi/authme/security/crypts/AbstractEncryptionMethodTest.java
ljacqu 5be3f8facc #1095 Update SMF hash algorithm to generate salt as SMF does
- The salt isn't used for password hashing but SMF requires that there be one to generate the authentication cookie. This does not yet enable registration from Minecraft: SMF has other non-null columns that need to be tackled. This is a first step.
2017-10-08 22:42:37 +02:00

239 lines
11 KiB
Java

package fr.xephi.authme.security.crypts;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import fr.xephi.authme.TestHelper;
import fr.xephi.authme.security.crypts.description.AsciiRestricted;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.List;
import java.util.Map;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeThat;
/**
* Test for implementations of {@link EncryptionMethod}.
*/
public abstract class AbstractEncryptionMethodTest {
/** The username used to query {@link EncryptionMethod#comparePassword}. */
private static final String USERNAME = "Test_Player00";
/**
* List of passwords whose hash is provided to the class to test against; this verifies that previously constructed
* hashes remain valid.
*/
private static final String[] GIVEN_PASSWORDS = {"password", "PassWord1", "&^%te$t?Pw@_", "âË_3(íù*"};
/**
* List of passwords that are hashed at runtime and then tested against; this verifies that newly generated hashes
* are valid.
*/
private static final List<String> INTERNAL_PASSWORDS =
ImmutableList.of("test1234", "Ab_C73", "(!#&$~`_-Aa0", "Ûïé1&?+A");
private static final String[] BOGUS_HASHES = {"", "test", "$t$test$", "$SHA$Test$$$$", "$$$$$",
"asdfg:hjkl", "::test", "~#$#~~~#$#~", "d41d8cd98f00b204e9800998ecf427e",
"$2y$7a$da641e404b982ed" };
/**
* Certain hash algorithms are slow by design, which has a considerable effect on these unit tests.
* Setting the property below to "true" will reduce the checks in these unit tests as to offer fast,
* partial tests during development.
*/
private static final boolean SKIP_LONG_TESTS =
"true".equals(System.getProperty("project.skipExtendedHashTests"));
/** The encryption method to test. */
private EncryptionMethod method;
/** Map with the hashes against which the entries in GIVEN_PASSWORDS are tested. */
private Map<String, HashedPassword> hashes;
/**
* Creates a new test for the given encryption method. This is for encryption methods that do not have
* a separate salt.
*
* @param method the encryption method to test
* @param hash0 the pre-generated hash for the first entry in {@link #GIVEN_PASSWORDS}
* @param hash1 hash for second given password
* @param hash2 hash for third given password
* @param hash3 hash for fourth given password
*/
public AbstractEncryptionMethodTest(EncryptionMethod method, String hash0,
String hash1, String hash2, String hash3) {
verifyCorrectConstructorIsUsed(method, false);
this.method = method;
hashes = ImmutableMap.of(
GIVEN_PASSWORDS[0], new HashedPassword(hash0),
GIVEN_PASSWORDS[1], new HashedPassword(hash1),
GIVEN_PASSWORDS[2], new HashedPassword(hash2),
GIVEN_PASSWORDS[3], new HashedPassword(hash3));
}
/**
* Creates a new test for the given encryption method. This is for encryption methods that use
* a separate salt.
*
* @param method the encryption method to test
* @param result0 the pre-generated hash for the first entry in {@link #GIVEN_PASSWORDS}
* @param result1 hash for second given password
* @param result2 hash for third given password
* @param result3 hash for fourth given password
*/
public AbstractEncryptionMethodTest(EncryptionMethod method, HashedPassword result0, HashedPassword result1,
HashedPassword result2, HashedPassword result3) {
verifyCorrectConstructorIsUsed(method, true);
this.method = method;
hashes = ImmutableMap.of(
GIVEN_PASSWORDS[0], result0,
GIVEN_PASSWORDS[1], result1,
GIVEN_PASSWORDS[2], result2,
GIVEN_PASSWORDS[3], result3);
}
@BeforeClass
public static void setupLogger() {
TestHelper.setupLogger();
}
protected void verifyCorrectConstructorIsUsed(EncryptionMethod method, boolean isConstructorWithSalt) {
if (isConstructorWithSalt && !method.hasSeparateSalt()) {
throw new UnsupportedOperationException("Salt is not stored separately, so test should be initialized"
+ " with the password hashes only. Use the other constructor");
} else if (!isConstructorWithSalt && method.hasSeparateSalt()) {
throw new UnsupportedOperationException("Test must be initialized with HashedPassword objects if "
+ "the salt is stored separately. Use the other constructor");
}
}
@Test
public void testGivenPasswords() {
// Start with the 2nd to last password if we skip long tests
int start = SKIP_LONG_TESTS ? GIVEN_PASSWORDS.length - 2 : 0;
// Test entries in GIVEN_PASSWORDS except the last one
for (int i = start; i < GIVEN_PASSWORDS.length - 1; ++i) {
String password = GIVEN_PASSWORDS[i];
assertTrue("Hash for password '" + password + "' should match",
doesGivenHashMatch(password, method));
}
// Note #375: Windows console seems to use its own character encoding (Windows-1252?) and it seems impossible to
// force it to use UTF-8, so passwords with non-ASCII characters will fail. Since we do not recommend to use
// such characters in passwords (something outside of our control, e.g. a database system, might also cause
// problems), we will check the last password in GIVEN_PASSWORDS in a non-failing way; if the hash doesn't match
// we'll just issue a message to System.err
String lastPassword = GIVEN_PASSWORDS[GIVEN_PASSWORDS.length - 1];
if (!doesGivenHashMatch(lastPassword, method)) {
System.err.println("Note: Hash for password '" + lastPassword + "' does not match for method " + method);
}
}
@Test
public void testPasswordEquality() {
List<String> internalPasswords = method.getClass().isAnnotationPresent(AsciiRestricted.class)
? INTERNAL_PASSWORDS.subList(0, INTERNAL_PASSWORDS.size() - 1)
: INTERNAL_PASSWORDS;
for (String password : internalPasswords) {
final String salt = method.generateSalt();
final String hash = method.computeHash(password, salt, USERNAME);
HashedPassword hashedPassword = new HashedPassword(hash, salt);
// Check that the computeHash(password, salt, name) method has the same output for the returned salt
if (testHashEqualityForSameSalt()) {
assertThat("Computing a hash with the same salt will generate the same hash",
hash, equalTo(method.computeHash(password, salt, USERNAME)));
}
assertTrue("Generated hash for '" + password + "' should match password (hash = '" + hash + "')",
method.comparePassword(password, hashedPassword, USERNAME));
assumeThat(SKIP_LONG_TESTS, equalTo(false));
if (!password.equals(password.toLowerCase())) {
assertFalse("Lower-case of '" + password + "' should not match generated hash '" + hash + "'",
method.comparePassword(password.toLowerCase(), hashedPassword, USERNAME));
}
if (!password.equals(password.toUpperCase())) {
assertFalse("Upper-case of '" + password + "' should not match generated hash '" + hash + "'",
method.comparePassword(password.toUpperCase(), hashedPassword, USERNAME));
}
}
}
/** Tests various strings to ensure that encryption methods don't rely on the hash's format too much. */
@Test
public void testMalformedHashes() {
assumeThat(SKIP_LONG_TESTS, equalTo(false));
String salt = method.hasSeparateSalt() ? "testSalt" : null;
for (String bogusHash : BOGUS_HASHES) {
HashedPassword hashedPwd = new HashedPassword(bogusHash, salt);
assertFalse("Passing bogus hash '" + bogusHash + "' does not result in an error",
method.comparePassword("Password", hashedPwd, "player"));
}
}
private boolean doesGivenHashMatch(String password, EncryptionMethod method) {
return method.comparePassword(password, hashes.get(password), USERNAME);
}
/**
* Generates a test class for a given encryption method. Simply create a test class and run the following code,
* replacing {@code XXX} with the actual class:
* <p>
* <code>@org.junit.Test public void generate() { AbstractEncryptionMethodTest.generateTest(new XXX()); }</code>
* <p>
* The output is the entire test class.
*
* @param method The method to create a test class for
*/
protected static void generateTest(EncryptionMethod method) {
String className = method.getClass().getSimpleName();
// Create javadoc and "public class extends" and the constructor call "super(new Class(),"
System.out.println("/**\n * Test for {@link " + className + "}.\n */");
System.out.println("public class " + className + "Test extends AbstractEncryptionMethodTest {");
System.out.println("\n\tpublic " + className + "Test() {");
System.out.println("\t\tsuper(new " + className + "(),");
// Iterate through the GIVEN_PASSWORDS and generate a hash so we can always check it later
String delim = ", ";
for (String password : GIVEN_PASSWORDS) {
if (password.equals(GIVEN_PASSWORDS[GIVEN_PASSWORDS.length - 1])) {
delim = "); ";
}
// Encr. method uses separate salt, so we need to call the constructor that takes HashedPassword instances
if (method.hasSeparateSalt()) {
HashedPassword hashedPassword = method.computeHash(password, USERNAME);
System.out.println(String.format("\t\tnew HashedPassword(\"%s\", \"%s\")%s// %s",
hashedPassword.getHash(), hashedPassword.getSalt(), delim, password));
} else {
// Encryption method doesn't have separate salt, so simply pass the generated hash to the constructor
System.out.println("\t\t\"" + method.computeHash(password, USERNAME).getHash()
+ "\"" + delim + "// " + password);
}
}
// Close the constructor and class declarations
System.out.println("\t}");
System.out.println("\n}");
}
/**
* Return whether an encryption algorithm should be tested that it generates the same
* hash for the same salt. If {@code true}, we call {@link EncryptionMethod#computeHash(String, String)}
* and verify that {@link EncryptionMethod#computeHash(String, String, String)} generates
* the same hash for the salt returned in the first call.
*
* @return Whether or not to test that the hash is the same for the same salt
*/
protected boolean testHashEqualityForSameSalt() {
return true;
}
}