- 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.
239 lines
11 KiB
Java
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;
|
|
}
|
|
|
|
}
|