Browse Source

Merge branch 'pingunaut-1166_more_secure_password_hashes' into master.

tags/r1.9.0
Florian Zschocke 4 years ago
parent
commit
719afbacd0

+ 5
- 3
src/main/distrib/data/defaults.properties View File

@@ -869,12 +869,14 @@ realm.userService = ${baseFolder}/users.conf
realm.authenticationProviders =

# How to store passwords.
# Valid values are plain, md5, or combined-md5. md5 is the hash of password.
# Valid values are plain, md5, combined-md5 or pbkdf2.
# md5 is the hash of password.
# combined-md5 is the hash of username.toLowerCase()+password.
# Default is md5.
# pbkdf2 implements the PBKDF2 algorithm, which is a secure, salted password hashing scheme.
# Default is pbkdf2. Using plain, md5 or combined-md5 is deprecated, as these are insecure schemes by now.
#
# SINCE 0.5.0
realm.passwordStorage = md5
realm.passwordStorage = pbkdf2

# Minimum valid length for a plain text password.
# Default value is 5. Absolute minimum is 4.

+ 14
- 12
src/main/java/com/gitblit/client/EditUserDialog.java View File

@@ -58,8 +58,10 @@ import com.gitblit.models.RepositoryModel;
import com.gitblit.models.ServerSettings;
import com.gitblit.models.TeamModel;
import com.gitblit.models.UserModel;
import com.gitblit.utils.PasswordHash;
import com.gitblit.utils.StringUtils;
public class EditUserDialog extends JDialog {
private static final long serialVersionUID = 1L;
@@ -317,8 +319,10 @@ public class EditUserDialog extends JDialog {
minLength));
return false;
}
if (!password.toUpperCase().startsWith(StringUtils.MD5_TYPE)
&& !password.toUpperCase().startsWith(StringUtils.COMBINED_MD5_TYPE)) {
// What we actually test for here, is if the password has been changed. But this also catches if the password
// was not changed, but is stored in plain-text. Which is good because then editing the user will hash the
// password if by now the storage has been changed to a hashed variant.
if (!PasswordHash.isHashedEntry(password)) {
String cpw = new String(confirmPasswordField.getPassword());
if (cpw == null || cpw.length() != password.length()) {
error("Please confirm the password!");
@@ -332,19 +336,17 @@ public class EditUserDialog extends JDialog {
// change the cookie
user.cookie = user.createCookie();
String type = settings.get(Keys.realm.passwordStorage).getString("md5");
if (type.equalsIgnoreCase("md5")) {
// store MD5 digest of password
user.password = StringUtils.MD5_TYPE + StringUtils.getMD5(password);
} else if (type.equalsIgnoreCase("combined-md5")) {
// store MD5 digest of username+password
user.password = StringUtils.COMBINED_MD5_TYPE
+ StringUtils.getMD5(user.username + password);
String type = settings.get(Keys.realm.passwordStorage).getString(PasswordHash.getDefaultType().name());
PasswordHash pwdHash = PasswordHash.instanceOf(type);
if (pwdHash != null) {
user.password = pwdHash.toHashedEntry(password, user.username);
} else {
// plain-text password
// plain-text password.
// TODO: This is also used when the "realm.passwordStorage" configuration is not a valid type.
// This is a rather bad default, and should probably caught and changed to a secure default.
user.password = password;
}
} else if (rename && password.toUpperCase().startsWith(StringUtils.COMBINED_MD5_TYPE)) {
} else if (rename && password.toUpperCase().startsWith(PasswordHash.Type.CMD5.name())) {
error("Gitblit is configured for combined-md5 password hashing. You must enter a new password on account rename.");
return false;
} else {

+ 38
- 12
src/main/java/com/gitblit/manager/AuthenticationManager.java View File

@@ -52,6 +52,7 @@ import com.gitblit.models.UserModel;
import com.gitblit.transport.ssh.SshKey;
import com.gitblit.utils.Base64;
import com.gitblit.utils.HttpUtils;
import com.gitblit.utils.PasswordHash;
import com.gitblit.utils.StringUtils;
import com.gitblit.utils.X509Utils.X509Metadata;
import com.google.inject.Inject;
@@ -518,26 +519,51 @@ public class AuthenticationManager implements IAuthenticationManager {
*/
protected UserModel authenticateLocal(UserModel user, char [] password) {
UserModel returnedUser = null;
if (user.password.startsWith(StringUtils.MD5_TYPE)) {
// password digest
String md5 = StringUtils.MD5_TYPE + StringUtils.getMD5(new String(password));
if (user.password.equalsIgnoreCase(md5)) {
returnedUser = user;
}
} else if (user.password.startsWith(StringUtils.COMBINED_MD5_TYPE)) {
// username+password digest
String md5 = StringUtils.COMBINED_MD5_TYPE
+ StringUtils.getMD5(user.username.toLowerCase() + new String(password));
if (user.password.equalsIgnoreCase(md5)) {

PasswordHash pwdHash = PasswordHash.instanceFor(user.password);
if (pwdHash != null) {
if (pwdHash.matches(user.password, password, user.username)) {
returnedUser = user;
}
} else if (user.password.equals(new String(password))) {
// plain-text password
returnedUser = user;
}
// validate user
returnedUser = validateAuthentication(returnedUser, AuthenticationType.CREDENTIALS);
// try to upgrade the stored password hash to a stronger hash, if necessary
upgradeStoredPassword(returnedUser, password, pwdHash);

return returnedUser;
}

/**
* Upgrade stored password to a strong hash if configured.
*
* @param user the user to be updated
* @param password the password
* @param pwdHash
* Instance of PasswordHash for the stored password entry. If null, no current hashing is assumed.
*/
private void upgradeStoredPassword(UserModel user, char[] password, PasswordHash pwdHash) {
// check if user has successfully authenticated i.e. is not null
if (user == null) return;

// check if strong hash algorithm is configured
String algorithm = settings.getString(Keys.realm.passwordStorage, PasswordHash.getDefaultType().name());
if (pwdHash == null || pwdHash.needsUpgradeTo(algorithm)) {
// rehash the provided correct password and update the user model
pwdHash = PasswordHash.instanceOf(algorithm);
if (pwdHash != null) { // necessary since the algorithm name could be something not supported.
user.password = pwdHash.toHashedEntry(password, user.username);
userManager.updateUserModel(user);
}
}
return validateAuthentication(returnedUser, AuthenticationType.CREDENTIALS);
}


/**
* Returns the Gitlbit cookie in the request.
*

+ 292
- 0
src/main/java/com/gitblit/utils/PasswordHash.java View File

@@ -0,0 +1,292 @@
/*
* Copyright 2017 gitblit.com.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.gitblit.utils;

import java.util.Arrays;

/**
* This is the superclass for classes responsible for handling password hashing.
*
* It provides a factory-like interface to create an instance of a class that
* is responsible for the mechanics of a specific password hashing method.
* It also provides the common interface, leaving implementation specifics
* to subclasses of itself, which are the factory products.
*
* @author Florian Zschocke
* @since 1.9.0
*/
public abstract class PasswordHash {

/**
* The types of implemented password hashing schemes.
*/
public enum Type {
MD5,
CMD5,
PBKDF2;

static Type fromName(String name) {
if (name == null) return null;
for (Type type : Type.values()) {
if (type.name().equalsIgnoreCase(name)) return type;
}
// Compatibility with type id "PBKDF2WITHHMACSHA256", which is also handled by PBKDF2 type.
if (name.equalsIgnoreCase("PBKDF2WITHHMACSHA256")) return Type.PBKDF2;

// Recognise the name used for CMD5 in the settings file.
if (name.equalsIgnoreCase("combined-md5")) return Type.CMD5;

return null;
}
}

/**
* The hashing scheme type handled by an instance of a subclass
*/
final Type type;


/**
* Constructor for subclasses to initialize the final type field.
* @param type
* Type of hashing scheme implemented by this instance.
*/
PasswordHash(Type type) {
this.type = type;
}


/**
* When no hash type is specified, this determines the default that should be used.
*/
public static Type getDefaultType() {
return Type.PBKDF2;
}


/**
* Create an instance of a password hashing class for the given hash type.
*
* @param type
* Type of hash to be used.
* @return A class that can calculate the given hash type and verify a user password,
* or null if the given hash type is not a valid one.
*/
public static PasswordHash instanceOf(String type) {
Type hashType = Type.fromName(type);
if (hashType == null) return null;
switch (hashType) {
case MD5:
return new PasswordHashMD5();
case CMD5:
return new PasswordHashCombinedMD5();
case PBKDF2:
return new PasswordHashPbkdf2();
default:
return null;
}
}

/**
* Create an instance of a password hashing class of the correct type for a given
* hashed password from the user password table. The stored hashed password needs
* to be prefixed with the hash type identifier.
*
* @param hashedEntry
* Hashed password string from the user table.
* @return
* A class that can calculate the given hash type and verify a user password,
* or null if no instance can be created for the hashed user password.
*/
public static PasswordHash instanceFor(String hashedEntry) {
Type type = getEntryType(hashedEntry);
if (type != null) return instanceOf(type.name());
return null;
}

/**
* Test if a given string is a hashed password entry. This method simply checks if the
* given string is prefixed by a known hash type identifier.
*
* @param storedPassword
* A stored user password.
* @return True if the given string is detected to be hashed with a known hash type,
* false otherwise.
*/
public static boolean isHashedEntry(String storedPassword) {
return null != getEntryType(storedPassword);
}


/**
* Some hash methods are considered more secure than others. This method determines for a certain type
* of password hash if it is inferior than a given other type and stored passwords should be
* upgraded to the given hashing method.
*
* @param algorithm
* Password hashing type to be checked if it is superior than the one of this instance.
* @return True, if the given type in parameter {@code algorithm} is better and stored passwords should be upgraded to it,
* false, otehrwise.
*/
public boolean needsUpgradeTo(String algorithm) {
Type hashType = Type.fromName(algorithm);
if (hashType == null) return false;
if (this.type == hashType) return false;

// Right now we keep it really simple. With the existing types, only PBKDF2 is considered secure,
// everything else is inferior. This will need to be updated once more secure hashing algorithms
// are implemented, or the workload/parameters of the PBKDF2 are changed.
return hashType == Type.PBKDF2;
}


/**
* Convert the given password to a hashed password entry to be stored in the user table.
* The resulting string is prefixed by the hashing scheme type followed by a colon:
* TYPE:theactualhashinhex
*
* @param password
* Password to be hashed.
* @param username
* User name, only used for the Combined-MD5 (user+MD5) hashing type.
* @return
* Hashed password entry to be stored in the user table.
*/
abstract public String toHashedEntry(char[] password, String username);

/**
* Convert the given password to a hashed password entry to be stored in the user table.
* The resulting string is prefixed by the hashing scheme type followed by a colon:
* TYPE:theactualhashinhex
*
* @param password
* Password to be hashed.
* @param username
* User name, only used for the Combined-MD5 (user+MD5) hashing type.
* @return
* Hashed password entry to be stored in the user table.
*/
public String toHashedEntry(String password, String username) {
if (password == null) throw new IllegalArgumentException("The password argument may not be null when hashing a password.");
return toHashedEntry(password.toCharArray(), username);
}

/**
* Test if a given password (and user name) match a hashed password.
* The instance of the password hash class has to be created with
* {code instanceFor}, so that it matches the type of the hashed password
* entry to test against.
*
*
* @param hashedEntry
* The hashed password entry from the user password table.
* @param password
* Clear text password to test against the hashed one.
* @param username
* User name, needed for the MD5+USER hash type.
* @return True, if the password (and username) match the hashed password,
* false, otherwise.
*/
public boolean matches(String hashedEntry, char[] password, String username) {
if (hashedEntry == null || type != PasswordHash.getEntryType(hashedEntry)) return false;
if (password == null) return false;

String hashed = toHashedEntry(password, username);
Arrays.fill(password, Character.MIN_VALUE);
return hashed.equalsIgnoreCase(hashedEntry);
}





static Type getEntryType(String hashedEntry) {
if (hashedEntry == null) return null;
int indexOfSeparator = hashedEntry.indexOf(':');
if (indexOfSeparator <= 0) return null;
String typeId = hashedEntry.substring(0, indexOfSeparator);
return Type.fromName(typeId);
}


static String getEntryValue(String hashedEntry) {
if (hashedEntry == null) return null;
int indexOfSeparator = hashedEntry.indexOf(':');
return hashedEntry.substring(indexOfSeparator +1);
}





/************************************** Implementations *************************************************/

private static class PasswordHashMD5 extends PasswordHash
{
PasswordHashMD5() {
super(Type.MD5);
}

// To keep the handling identical to how it was before, and therefore not risk invalidating stored passwords,
// for MD5 the (String,String) variant of the method is the one implementing the hashing.
@Override
public String toHashedEntry(char[] password, String username) {
if (password == null) throw new IllegalArgumentException("The password argument may not be null when hashing a password.");
return toHashedEntry(new String(password), username);
}

@Override
public String toHashedEntry(String password, String username) {
if (password == null) throw new IllegalArgumentException("The password argument may not be null when hashing a password.");
return type.name() + ":"
+ StringUtils.getMD5(password);
}
}




private static class PasswordHashCombinedMD5 extends PasswordHash
{
PasswordHashCombinedMD5() {
super(Type.CMD5);
}

// To keep the handling identical to how it was before, and therefore not risk invalidating stored passwords,
// for Combined-MD5 the (String,String) variant of the method is the one implementing the hashing.
@Override
public String toHashedEntry(char[] password, String username) {
if (password == null) throw new IllegalArgumentException("The password argument may not be null when hashing a password.");
return toHashedEntry(new String(password), username);
}
@Override
public String toHashedEntry(String password, String username) {
if (password == null) throw new IllegalArgumentException("The password argument may not be null when hashing a password.");
if (username == null) throw new IllegalArgumentException("The username argument may not be null when hashing a password with Combined-MD5.");
if (StringUtils.isEmpty(username)) throw new IllegalArgumentException("The username argument may not be empty when hashing a password with Combined-MD5.");
return type.name() + ":"
+ StringUtils.getMD5(username.toLowerCase() + password);
}

@Override
public boolean matches(String hashedEntry, char[] password, String username) {
if (username == null || StringUtils.isEmpty(username)) return false;
return super.matches(hashedEntry, password, username);
}

}
}

+ 276
- 0
src/main/java/com/gitblit/utils/PasswordHashPbkdf2.java View File

@@ -0,0 +1,276 @@
package com.gitblit.utils;

import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.util.Arrays;

/**
* The class PasswordHashPbkdf2 implements password hashing and validation
* with PBKDF2
*
* It uses the concept proposed by OWASP - Hashing Java:
* https://www.owasp.org/index.php/Hashing_Java
*/
class PasswordHashPbkdf2 extends PasswordHash
{

private static final Logger LOGGER = LoggerFactory.getLogger(PasswordHashPbkdf2.class);

/**
* The PBKDF has some parameters that define security and workload.
* The Configuration class keeps these parameters.
*/
private static class Configuration
{
private final String algorithm;
private final int iterations;
private final int keyLen;
private final int saltLen;

private Configuration(String algorithm, int iterations, int keyLen, int saltLen) {
this.algorithm = algorithm;
this.iterations = iterations;
this.keyLen = keyLen;
this.saltLen = saltLen;
}
}


private static final SecureRandom RANDOM = new SecureRandom();
/**
* A list of Configurations is created to list the configurations supported by
* this implementation. The configuration id is stored in the hashed entry,
* identifying the Configuration in this array.
* When adding a new variant with different values for these parameters, add
* it to this array.
* The code uses the last configuration in the array as the most secure, to be used
* when creating new hashes when no configuration is specified.
*/
private static final Configuration[] configurations = {
// Configuration 0, also default when none is specified in the stored hashed entry.
new Configuration("PBKDF2WithHmacSHA256", 10000, 256, 32)
};


PasswordHashPbkdf2() {
super(Type.PBKDF2);
}


/*
* We return a hashed entry, where the hash part (salt+hash) itself is prefixed
* again by the configuration id of the configuration that was used for the PBKDF,
* enclosed in '$':
* PBKDF2:$0$thesaltThehash
*/
@Override
public String toHashedEntry(char[] password, String username) {
if (password == null) {
LOGGER.warn("The password argument may not be null when hashing a password.");
throw new IllegalArgumentException("The password argument may not be null when hashing a password.");
}

int configId = getLatestConfigurationId();
Configuration config = configurations[configId];

byte[] salt = new byte[config.saltLen];
RANDOM.nextBytes(salt);
byte[] hash = hash(password, salt, config);

return type.name() + ":"
+ "$" + configId + "$"
+ StringUtils.toHex(salt)
+ StringUtils.toHex(hash);
}

@Override
public boolean matches(String hashedEntry, char[] password, String username) {
if (hashedEntry == null || type != PasswordHash.getEntryType(hashedEntry)) return false;
if (password == null) return false;

String hashedPart = getEntryValue(hashedEntry);
int configId = getConfigIdFromStoredPassword(hashedPart);

return isPasswordCorrect(password, hashedPart, configurations[configId]);
}








/**
* Return the id of the most updated configuration of parameters for the PBKDF.
* New password hashes should be generated with this one.
*
* @return An index into the configurations array for the latest configuration.
*/
private int getLatestConfigurationId() {
return configurations.length-1;
}


/**
* Get the configuration id from the stored hashed password, that was used when the
* hash was created. The configuration id is the index into the configuration array,
* and is stored in the format $Id$ after the type identifier: TYPE:$Id$....
* If there is no identifier in the stored entry, id 0 is used, to keep backward
* compatibility.
* If an id is found that is not in the range of the declared configurations,
* 0 is returned. This may fail password validation. As of now there is only one
* configuration and even if there were more, chances are slim that anything else
* was used. So we try at least the first one instead of failing with an exception
* as the probability of success is high enough to save the user from a bad experience
* and to risk some hassle for the admin finding out in the logs why a login failed,
* when it does.
*
* @param hashPart
* The hash part of the stored entry, i.e. the part after the TYPE:
* @return The configuration id, or
* 0 if none was found.
*/
private static int getConfigIdFromStoredPassword(String hashPart) {
String[] parts = hashPart.split("\\$", 3);
// If there are not two parts, there is no '$'-enclosed part and we have no configuration information stored.
// Return default 0.
if (parts.length <= 2) return 0;

// The first string wil be empty. Even if it isn't we ignore it because it doesn't contain our information.
try {
int configId = Integer.parseInt(parts[1]);
if (configId < 0 || configId >= configurations.length) {
LOGGER.warn("A user table password entry contains a configuration id that is not valid: {}." +
"Assuming PBKDF configuration 0. This may fail to validate the password.", configId);
return 0;
}
return configId;
}
catch (NumberFormatException e) {
LOGGER.warn("A user table password entry contains a configuration id that is not a parsable number ({}${}$...)." +
"Assuming PBKDF configuration 0. This may fail to validate the password.", parts[0], parts[1], e);
return 0;
}
}





/**
* Hash.
*
* @param password
* the password
* @param salt
* the salt
* @param config
* Parameter configuration to use for the PBKDF
* @return Hashed result
*/
private static byte[] hash(char[] password, byte[] salt, Configuration config) {
PBEKeySpec spec = new PBEKeySpec(password, salt, config.iterations, config.keyLen);
Arrays.fill(password, Character.MIN_VALUE);
try {
SecretKeyFactory skf = SecretKeyFactory.getInstance(config.algorithm);
return skf.generateSecret(spec).getEncoded();
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
LOGGER.warn("Error while hashing password.", e);
throw new IllegalStateException("Error while hashing password", e);
} finally {
spec.clearPassword();
}
}

/**
* Checks if is password correct.
*
* @param passwordToCheck
* the password to check
* @param salt
* the salt
* @param expectedHash
* the expected hash
* @return true, if is password correct
*/
private static boolean isPasswordCorrect(char[] passwordToCheck, byte[] salt, byte[] expectedHash, Configuration config) {
byte[] hashToCheck = hash(passwordToCheck, salt, config);
Arrays.fill(passwordToCheck, Character.MIN_VALUE);
if (hashToCheck.length != expectedHash.length) {
return false;
}
for (int i = 0; i < hashToCheck.length; i++) {
if (hashToCheck[i] != expectedHash[i]) {
return false;
}
}
return true;
}


/**
* Gets the salt from stored password.
*
* @param storedPassword
* the stored password
* @return the salt from stored password
*/
private static byte[] getSaltFromStoredPassword(String storedPassword, Configuration config) {
byte[] pw = getStoredHashWithStrippedPrefix(storedPassword);
return Arrays.copyOfRange(pw, 0, config.saltLen);
}

/**
* Gets the hash from stored password.
*
* @param storedPassword
* the stored password
* @return the hash from stored password
*/
private static byte[] getHashFromStoredPassword(String storedPassword, Configuration config) {
byte[] pw = getStoredHashWithStrippedPrefix(storedPassword);
return Arrays.copyOfRange(pw, config.saltLen, pw.length);
}

/**
* Strips the configuration id prefix ($Id$) from a stored
* password and returns the decoded hash
*
* @param storedPassword
* the stored password
* @return the stored hash with stripped prefix
*/
private static byte[] getStoredHashWithStrippedPrefix(String storedPassword) {
String[] strings = storedPassword.split("\\$", 3);
String saltAndHash = strings[strings.length -1];
try {
return Hex.decodeHex(saltAndHash.toCharArray());
} catch (DecoderException e) {
LOGGER.warn("Failed to decode stored password entry from hex to string.", e);
throw new IllegalStateException("Error while reading stored credentials", e);
}
}

/**
* Checks if password is correct.
*
* @param password
* the password to validate
* @param storedPassword
* the stored password, i.e. the password entry value, without the leading TYPE:
* @return true, if password is correct, false otherwise
*/
private static boolean isPasswordCorrect(char[] password, String storedPassword, Configuration config) {
byte[] storedSalt = getSaltFromStoredPassword(storedPassword, config);
byte[] storedHash = getHashFromStoredPassword(storedPassword, config);
return isPasswordCorrect(password, storedSalt, storedHash, config);
}
}

+ 0
- 4
src/main/java/com/gitblit/utils/StringUtils.java View File

@@ -46,10 +46,6 @@ import java.util.regex.PatternSyntaxException;
*/
public class StringUtils {
public static final String MD5_TYPE = "MD5:";
public static final String COMBINED_MD5_TYPE = "CMD5:";
/**
* Returns true if the string is null or empty.
*

+ 8
- 12
src/main/java/com/gitblit/wicket/pages/ChangePasswordPage.java View File

@@ -28,14 +28,14 @@ import org.apache.wicket.protocol.http.WebResponse;
import com.gitblit.GitBlitException;
import com.gitblit.Keys;
import com.gitblit.models.UserModel;
import com.gitblit.utils.StringUtils;
import com.gitblit.utils.PasswordHash;
import com.gitblit.wicket.GitBlitWebSession;
import com.gitblit.wicket.NonTrimmedPasswordTextField;
public class ChangePasswordPage extends RootSubPage {
IModel<String> password = new Model<String>("");
IModel<String> confirmPassword = new Model<String>("");
private IModel<String> password = new Model<String>("");
private IModel<String> confirmPassword = new Model<String>("");
public ChangePasswordPage() {
super();
@@ -85,15 +85,11 @@ public class ChangePasswordPage extends RootSubPage {
UserModel user = GitBlitWebSession.get().getUser();
// convert to MD5 digest, if appropriate
String type = app().settings().getString(Keys.realm.passwordStorage, "md5");
if (type.equalsIgnoreCase("md5")) {
// store MD5 digest of password
password = StringUtils.MD5_TYPE + StringUtils.getMD5(password);
} else if (type.equalsIgnoreCase("combined-md5")) {
// store MD5 digest of username+password
password = StringUtils.COMBINED_MD5_TYPE
+ StringUtils.getMD5(user.username.toLowerCase() + password);
// convert to digest, if appropriate
String type = app().settings().getString(Keys.realm.passwordStorage, PasswordHash.getDefaultType().name());
PasswordHash pwdHash = PasswordHash.instanceOf(type);
if (pwdHash != null) {
password = pwdHash.toHashedEntry(password, user.username);
}
user.password = password;

+ 8
- 13
src/main/java/com/gitblit/wicket/pages/EditUserPage.java View File

@@ -22,6 +22,7 @@ import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import com.gitblit.utils.PasswordHash;
import org.apache.wicket.PageParameters;
import org.apache.wicket.behavior.SimpleAttributeModifier;
import org.apache.wicket.extensions.markup.html.form.palette.Palette;
@@ -172,15 +173,14 @@ public class EditUserPage extends RootSubPage {
return;
}
String password = userModel.password;
if (!password.toUpperCase().startsWith(StringUtils.MD5_TYPE)
&& !password.toUpperCase().startsWith(StringUtils.COMBINED_MD5_TYPE)) {
if (!PasswordHash.isHashedEntry(password)) {
// This is a plain text password.
// Check length.
int minLength = app().settings().getInteger(Keys.realm.minPasswordLength, 5);
if (minLength < 4) {
minLength = 4;
}
if (password.trim().length() < minLength) {
if (password.trim().length() < minLength) { // TODO: Why do we trim here, but not in EditUserDialog and ChangePasswordPage?
error(MessageFormat.format(getString("gb.passwordTooShort"),
minLength));
return;
@@ -190,18 +190,13 @@ public class EditUserPage extends RootSubPage {
userModel.cookie = userModel.createCookie();
// Optionally store the password MD5 digest.
String type = app().settings().getString(Keys.realm.passwordStorage, "md5");
if (type.equalsIgnoreCase("md5")) {
// store MD5 digest of password
userModel.password = StringUtils.MD5_TYPE
+ StringUtils.getMD5(userModel.password);
} else if (type.equalsIgnoreCase("combined-md5")) {
// store MD5 digest of username+password
userModel.password = StringUtils.COMBINED_MD5_TYPE
+ StringUtils.getMD5(username + userModel.password);
String type = app().settings().getString(Keys.realm.passwordStorage, PasswordHash.getDefaultType().name());
PasswordHash pwdh = PasswordHash.instanceOf(type);
if (pwdh != null) { // Hash the password
userModel.password = pwdh.toHashedEntry(password, username);
}
} else if (rename
&& password.toUpperCase().startsWith(StringUtils.COMBINED_MD5_TYPE)) {
&& password.toUpperCase().startsWith(PasswordHash.Type.CMD5.name())) {
error(getString("gb.combinedMd5Rename"));
return;
}

+ 1
- 1
src/site/administration.mkd View File

@@ -169,7 +169,7 @@ Usernames must be unique and are case-insensitive.
Whitespace is illegal.
### Passwords
User passwords are CASE-SENSITIVE and may be *plain*, *md5*, or *combined-md5* formatted (see `gitblit.properties` -> *realm.passwordStorage*).
User passwords are CASE-SENSITIVE and may be *plain*, *md5*, *combined-md5* or *pbkdf2* formatted (see `gitblit.properties` -> *realm.passwordStorage*).
### User Roles
There are four actual *roles* in Gitblit:

+ 32
- 0
src/test/java/com/gitblit/tests/AuthenticationManagerTest.java View File

@@ -43,6 +43,7 @@ import javax.servlet.http.HttpSessionContext;
import javax.servlet.http.HttpUpgradeHandler;
import javax.servlet.http.Part;

import com.gitblit.utils.PasswordHash;
import org.junit.Test;

import com.gitblit.IUserService;
@@ -665,6 +666,37 @@ public class AuthenticationManagerTest extends GitblitUnitTest {
users.deleteUserModel(user);
}


@Test
public void testAuthenticateUpgradePlaintext() throws Exception {
IAuthenticationManager auth = newAuthenticationManager();

UserModel user = new UserModel("sunnyjim");
user.password = "password";
users.updateUserModel(user);

assertNotNull(auth.authenticate(user.username, user.password.toCharArray(), null));

// validate that plaintext password was automatically updated to hashed one
assertTrue(user.password.startsWith(PasswordHash.getDefaultType().name() + ":"));
}


@Test
public void testAuthenticateUpgradeMD5() throws Exception {
IAuthenticationManager auth = newAuthenticationManager();

UserModel user = new UserModel("sunnyjim");
user.password = "MD5:5F4DCC3B5AA765D61D8327DEB882CF99";
users.updateUserModel(user);

assertNotNull(auth.authenticate(user.username, "password".toCharArray(), null));

// validate that MD5 password was automatically updated to hashed one
assertTrue(user.password.startsWith(PasswordHash.getDefaultType().name() + ":"));
}


@Test
public void testContenairAuthenticate() throws Exception {
settings.put(Keys.realm.container.autoCreateAccounts, "true");

+ 666
- 0
src/test/java/com/gitblit/utils/PasswordHashTest.java View File

@@ -0,0 +1,666 @@
/*
* Copyright 2017 gitblit.com.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.gitblit.utils;

import static org.junit.Assert.*;

import org.junit.Test;

/**
* @author Florian Zschocke
*
*/
public class PasswordHashTest {

static final String MD5_PASSWORD_0 = "password";
static final String MD5_HASHED_ENTRY_0 = "MD5:5F4DCC3B5AA765D61D8327DEB882CF99";
static final String MD5_PASSWORD_1 = "This is a test password";
static final String MD5_HASHED_ENTRY_1 = "md5:8e1901831af502c0f842d4efb9083bcf";
static final String MD5_PASSWORD_2 = "版本库管理方案";
static final String MD5_HASHED_ENTRY_2 = "MD5:980017891ff67cf8a20f23aa810e7b5a";
static final String MD5_PASSWORD_3 = "PÿrâṃiĐ";
static final String MD5_HASHED_ENTRY_3 = "MD5:60359b7e22941164708ae2040040521f";

static final String CMD5_USERNAME_0 = "Jane Doe";
static final String CMD5_PASSWORD_0 = "password";
static final String CMD5_HASHED_ENTRY_0 = "CMD5:DB9639A6E5F21457F9DFD7735FAFA68B";
static final String CMD5_USERNAME_1 = "Joe Black";
static final String CMD5_PASSWORD_1 = "ThisIsAWeirdScheme.Weird";
static final String CMD5_HASHED_ENTRY_1 = "cmd5:5c154768287e32fa605656b98894da89";
static final String CMD5_USERNAME_2 = "快速便";
static final String CMD5_PASSWORD_2 = "版本库管理方案";
static final String CMD5_HASHED_ENTRY_2 = "CMD5:f38575ee8af23ba6d923c0d98ee767fc";
static final String CMD5_USERNAME_3 = "İńa";
static final String CMD5_PASSWORD_3 = "PÿrâṃiĐ";
static final String CMD5_HASHED_ENTRY_3 = "CMD5:f1cdc2348c907677529e0e1b011f6793";

static final String PBKDF2_PASSWORD_0 = "password";
static final String PBKDF2_HASHED_ENTRY_0 = "PBKDF2:70617373776f726450415353574f524470617373776f726450415353574f52440f17d16621b32ae1bb2b1041fcb19e294b35d514d361c08eed385766e38f6f3a";
static final String PBKDF2_PASSWORD_1 = "A REALLY better scheme than MD5";
static final String PBKDF2_HASHED_ENTRY_1 = "PBKDF2:$0$46726573682066726f6d207468652053414c54206d696e65206f6620446f6f6de8e50b035679b25ce8b6ab41440938b7b1f97fc0c797fcf59302c2916f6c8fef";
static final String PBKDF2_PASSWORD_2 = "passwordPASSWORDpassword";
static final String PBKDF2_HASHED_ENTRY_2 = "pbkdf2:$0$73616c7453414c5473616c7453414c5473616c7453414c5473616c7453414c54560d0f02b565e37695da15141044506d54cb633a5a70b41c574069ea50a1247a";
static final String PBKDF2_PASSWORD_3 = "foo";
static final String PBKDF2_HASHED_ENTRY_3 = "PBKDF2WITHHMACSHA256:2d7d3ccaa277787f288e9f929247361bfc83607c6a8447bf496267512e360ba0a97b3114937213b23230072517d65a2e00695a1cbc47a732510840817f22c1bc";



/**
* Test method for {@link com.gitblit.utils.PasswordHash#instanceOf(java.lang.String)} for MD5.
*/
@Test
public void testInstanceOfMD5() {

PasswordHash pwdh = PasswordHash.instanceOf("md5");
assertNotNull(pwdh);
assertEquals(PasswordHash.Type.MD5, pwdh.type);
assertTrue("Failed to match " +MD5_HASHED_ENTRY_1, pwdh.matches(MD5_HASHED_ENTRY_1, MD5_PASSWORD_1.toCharArray(), null));

pwdh = PasswordHash.instanceOf("MD5");
assertNotNull(pwdh);
assertEquals(PasswordHash.Type.MD5, pwdh.type);
assertTrue("Failed to match " +MD5_HASHED_ENTRY_0, pwdh.matches(MD5_HASHED_ENTRY_0, MD5_PASSWORD_0.toCharArray(), null));

pwdh = PasswordHash.instanceOf("mD5");
assertNotNull(pwdh);
assertEquals(PasswordHash.Type.MD5, pwdh.type);
assertTrue("Failed to match " +MD5_HASHED_ENTRY_1, pwdh.matches(MD5_HASHED_ENTRY_1, MD5_PASSWORD_1.toCharArray(), null));


pwdh = PasswordHash.instanceOf("CMD5");
assertNotNull(pwdh);
assertNotEquals(PasswordHash.Type.MD5, pwdh.type);
assertFalse("Failed to match " +MD5_HASHED_ENTRY_1, pwdh.matches(MD5_HASHED_ENTRY_1, MD5_PASSWORD_1.toCharArray(), null));
}



/**
* Test method for {@link com.gitblit.utils.PasswordHash#instanceOf(java.lang.String)} for combined MD5.
*/
@Test
public void testInstanceOfCombinedMD5() {

PasswordHash pwdh = PasswordHash.instanceOf("cmd5");
assertNotNull(pwdh);
assertEquals(PasswordHash.Type.CMD5, pwdh.type);
assertTrue("Failed to match " +CMD5_HASHED_ENTRY_1, pwdh.matches(CMD5_HASHED_ENTRY_1, CMD5_PASSWORD_1.toCharArray(), CMD5_USERNAME_1));

pwdh = PasswordHash.instanceOf("cMD5");
assertNotNull(pwdh);
assertEquals(PasswordHash.Type.CMD5, pwdh.type);
assertTrue("Failed to match " +CMD5_HASHED_ENTRY_1, pwdh.matches(CMD5_HASHED_ENTRY_1, CMD5_PASSWORD_1.toCharArray(), CMD5_USERNAME_1));

pwdh = PasswordHash.instanceOf("CMD5");
assertNotNull(pwdh);
assertEquals(PasswordHash.Type.CMD5, pwdh.type);
assertTrue("Failed to match " +CMD5_HASHED_ENTRY_0, pwdh.matches(CMD5_HASHED_ENTRY_0, CMD5_PASSWORD_0.toCharArray(), CMD5_USERNAME_0));


pwdh = PasswordHash.instanceOf("combined-md5");
assertNotNull(pwdh);
assertEquals(PasswordHash.Type.CMD5, pwdh.type);

pwdh = PasswordHash.instanceOf("COMBINED-MD5");
assertNotNull(pwdh);
assertEquals(PasswordHash.Type.CMD5, pwdh.type);


pwdh = PasswordHash.instanceOf("MD5");
assertNotNull(pwdh);
assertNotEquals(PasswordHash.Type.CMD5, pwdh.type);
assertFalse("Failed to match " +CMD5_HASHED_ENTRY_1, pwdh.matches(CMD5_HASHED_ENTRY_1, CMD5_PASSWORD_1.toCharArray(), CMD5_USERNAME_1));
}



/**
* Test method for {@link com.gitblit.utils.PasswordHash#instanceOf(java.lang.String)} for PBKDF2.
*/
@Test
public void testInstanceOfPBKDF2() {
PasswordHash pwdh = PasswordHash.instanceOf("PBKDF2");
assertNotNull(pwdh);
assertEquals(PasswordHash.Type.PBKDF2, pwdh.type);
assertTrue("Failed to match " +PBKDF2_HASHED_ENTRY_0, pwdh.matches(PBKDF2_HASHED_ENTRY_0, PBKDF2_PASSWORD_0.toCharArray(), null));

pwdh = PasswordHash.instanceOf("pbkdf2");
assertNotNull(pwdh);
assertEquals(PasswordHash.Type.PBKDF2, pwdh.type);
assertTrue("Failed to match " +PBKDF2_HASHED_ENTRY_1, pwdh.matches(PBKDF2_HASHED_ENTRY_1, PBKDF2_PASSWORD_1.toCharArray(), null));

pwdh = PasswordHash.instanceOf("pbKDF2");
assertNotNull(pwdh);
assertEquals(PasswordHash.Type.PBKDF2, pwdh.type);
assertTrue("Failed to match " +PBKDF2_HASHED_ENTRY_1, pwdh.matches(PBKDF2_HASHED_ENTRY_1, PBKDF2_PASSWORD_1.toCharArray(), null));


pwdh = PasswordHash.instanceOf("md5");
assertNotNull(pwdh);
assertNotEquals(PasswordHash.Type.PBKDF2, pwdh.type);
assertFalse("Failed to match " +PBKDF2_HASHED_ENTRY_1, pwdh.matches(PBKDF2_HASHED_ENTRY_1, PBKDF2_PASSWORD_1.toCharArray(), null));
}




/**
* Test that no instance is returned for plaintext or unknown or not
* yet implemented hashing schemes.
*/
@Test
public void testNoInstanceOf() {
PasswordHash pwdh = PasswordHash.instanceOf("plain");
assertNull(pwdh);

pwdh = PasswordHash.instanceOf("PLAIN");
assertNull(pwdh);

pwdh = PasswordHash.instanceOf("Plain");
assertNull(pwdh);

pwdh = PasswordHash.instanceOf("scrypt");
assertNull(pwdh);

pwdh = PasswordHash.instanceOf("bCrypt");
assertNull(pwdh);

pwdh = PasswordHash.instanceOf("BCRYPT");
assertNull(pwdh);

pwdh = PasswordHash.instanceOf("nixe");
assertNull(pwdh);

pwdh = PasswordHash.instanceOf(null);
assertNull(pwdh);
}



/**
* Test that for all known hash types an instance is created for a hashed entry
* that can verify the known password.
*
* Test method for {@link com.gitblit.utils.PasswordHash#instanceFor(java.lang.String)}.
*/
@Test
public void testInstanceFor() {
PasswordHash pwdh = PasswordHash.instanceFor(MD5_HASHED_ENTRY_0);
assertNotNull(pwdh);
assertEquals(PasswordHash.Type.MD5, pwdh.type);
assertTrue("Failed to match " +MD5_HASHED_ENTRY_0, pwdh.matches(MD5_HASHED_ENTRY_0, MD5_PASSWORD_0.toCharArray(), null));

pwdh = PasswordHash.instanceFor(MD5_HASHED_ENTRY_1);
assertNotNull(pwdh);
assertEquals(PasswordHash.Type.MD5, pwdh.type);
assertTrue("Failed to match " +MD5_HASHED_ENTRY_1, pwdh.matches(MD5_HASHED_ENTRY_1, MD5_PASSWORD_1.toCharArray(), null));


pwdh = PasswordHash.instanceFor(CMD5_HASHED_ENTRY_0);
assertNotNull(pwdh);
assertEquals(PasswordHash.Type.CMD5, pwdh.type);
assertTrue("Failed to match " +CMD5_HASHED_ENTRY_0, pwdh.matches(CMD5_HASHED_ENTRY_0, CMD5_PASSWORD_0.toCharArray(), CMD5_USERNAME_0));

pwdh = PasswordHash.instanceFor(CMD5_HASHED_ENTRY_1);
assertNotNull(pwdh);
assertEquals(PasswordHash.Type.CMD5, pwdh.type);
assertTrue("Failed to match " +CMD5_HASHED_ENTRY_1, pwdh.matches(CMD5_HASHED_ENTRY_1, CMD5_PASSWORD_1.toCharArray(), CMD5_USERNAME_1));


pwdh = PasswordHash.instanceFor(PBKDF2_HASHED_ENTRY_0);
assertNotNull(pwdh);
assertEquals(PasswordHash.Type.PBKDF2, pwdh.type);
assertTrue("Failed to match " +PBKDF2_HASHED_ENTRY_0, pwdh.matches(PBKDF2_HASHED_ENTRY_0, PBKDF2_PASSWORD_0.toCharArray(), null));

pwdh = PasswordHash.instanceFor(PBKDF2_HASHED_ENTRY_1);
assertNotNull(pwdh);
assertEquals(PasswordHash.Type.PBKDF2, pwdh.type);
assertTrue("Failed to match " +PBKDF2_HASHED_ENTRY_1, pwdh.matches(PBKDF2_HASHED_ENTRY_1, PBKDF2_PASSWORD_1.toCharArray(), null));

pwdh = PasswordHash.instanceFor(PBKDF2_HASHED_ENTRY_3);
assertNotNull(pwdh);
assertEquals(PasswordHash.Type.PBKDF2, pwdh.type);
assertTrue("Failed to match " +PBKDF2_HASHED_ENTRY_3, pwdh.matches(PBKDF2_HASHED_ENTRY_3, PBKDF2_PASSWORD_3.toCharArray(), null));
}

/**
* Test that for no instance is returned for plaintext or unknown or
* not yet implemented hashing schemes.
*
* Test method for {@link com.gitblit.utils.PasswordHash#instanceFor(java.lang.String)}.
*/
@Test
public void testInstanceForNaught() {
PasswordHash pwdh = PasswordHash.instanceFor("password");
assertNull(pwdh);

pwdh = PasswordHash.instanceFor("top secret");
assertNull(pwdh);

pwdh = PasswordHash.instanceFor("pass:word");
assertNull(pwdh);

pwdh = PasswordHash.instanceFor("PLAIN:password");
assertNull(pwdh);

pwdh = PasswordHash.instanceFor("SCRYPT:1232rwv12w");
assertNull(pwdh);

pwdh = PasswordHash.instanceFor("BCRYPT:urbvahiaufbvhabaiuevuzggubsbliue");
assertNull(pwdh);

pwdh = PasswordHash.instanceFor("");
assertNull(pwdh);

pwdh = PasswordHash.instanceFor(null);
assertNull(pwdh);
}


/**
* Test method for {@link com.gitblit.utils.PasswordHash#isHashedEntry(java.lang.String)}.
*/
@Test
public void testIsHashedEntry() {
assertTrue(MD5_HASHED_ENTRY_0, PasswordHash.isHashedEntry(MD5_HASHED_ENTRY_0));
assertTrue(MD5_HASHED_ENTRY_1, PasswordHash.isHashedEntry(MD5_HASHED_ENTRY_1));
assertTrue(CMD5_HASHED_ENTRY_0, PasswordHash.isHashedEntry(CMD5_HASHED_ENTRY_0));
assertTrue(CMD5_HASHED_ENTRY_1, PasswordHash.isHashedEntry(CMD5_HASHED_ENTRY_1));
assertTrue(PBKDF2_HASHED_ENTRY_0, PasswordHash.isHashedEntry(PBKDF2_HASHED_ENTRY_0));
assertTrue(PBKDF2_HASHED_ENTRY_1, PasswordHash.isHashedEntry(PBKDF2_HASHED_ENTRY_1));
assertTrue(PBKDF2_HASHED_ENTRY_2, PasswordHash.isHashedEntry(PBKDF2_HASHED_ENTRY_2));
assertTrue(PBKDF2_HASHED_ENTRY_3, PasswordHash.isHashedEntry(PBKDF2_HASHED_ENTRY_3));

assertFalse(MD5_PASSWORD_1, PasswordHash.isHashedEntry(MD5_PASSWORD_1));
assertFalse("topsecret", PasswordHash.isHashedEntry("topsecret"));
assertFalse("top:secret", PasswordHash.isHashedEntry("top:secret"));
assertFalse("secret Password", PasswordHash.isHashedEntry("secret Password"));
assertFalse("Empty string", PasswordHash.isHashedEntry(""));
assertFalse("Null", PasswordHash.isHashedEntry(null));
}

/**
* Test that hashed entry detection is not case sensitive on the hash type identifier.
*
* Test method for {@link com.gitblit.utils.PasswordHash#isHashedEntry(java.lang.String)}.
*/
@Test
public void testIsHashedEntryCaseInsenitive() {
assertTrue(MD5_HASHED_ENTRY_1.toLowerCase(), PasswordHash.isHashedEntry(MD5_HASHED_ENTRY_1.toLowerCase()));
assertTrue(CMD5_HASHED_ENTRY_1.toLowerCase(), PasswordHash.isHashedEntry(CMD5_HASHED_ENTRY_1.toLowerCase()));
assertTrue(PBKDF2_HASHED_ENTRY_1.toLowerCase(), PasswordHash.isHashedEntry(PBKDF2_HASHED_ENTRY_1.toLowerCase()));
assertTrue(PBKDF2_HASHED_ENTRY_3.toLowerCase(), PasswordHash.isHashedEntry(PBKDF2_HASHED_ENTRY_3.toLowerCase()));
}

/**
* Test that unknown or not yet implemented hashing schemes are not detected as hashed entries.
*
* Test method for {@link com.gitblit.utils.PasswordHash#isHashedEntry(java.lang.String)}.
*/
@Test
public void testIsHashedEntryUnknown() {
assertFalse("BCRYPT:thisismypassword", PasswordHash.isHashedEntry("BCRYPT:thisismypassword"));
assertFalse("TSTHSH:asdchabufzuzfbhbakrzburzbcuzkuzcbajhbcasjdhbckajsbc", PasswordHash.isHashedEntry("TSTHSH:asdchabufzuzfbhbakrzburzbcuzkuzcbajhbcasjdhbckajsbc"));
}




/**
* Test creating a hashed entry for scheme MD5. In this scheme there is no salt, so a direct
* comparison to a constant value is possible.
*
* Test method for {@link PasswordHash#toHashedEntry(String, String)} for MD5.
*/
@Test
public void testToHashedEntryMD5() {
PasswordHash pwdh = PasswordHash.instanceOf("MD5");
String hashedEntry = pwdh.toHashedEntry(MD5_PASSWORD_1, null);
assertTrue(MD5_HASHED_ENTRY_1.equalsIgnoreCase(hashedEntry));

hashedEntry = pwdh.toHashedEntry(MD5_PASSWORD_2, null);
assertTrue(MD5_HASHED_ENTRY_2.equalsIgnoreCase(hashedEntry));

hashedEntry = pwdh.toHashedEntry(MD5_PASSWORD_1, "charlie");
assertTrue(MD5_HASHED_ENTRY_1.equalsIgnoreCase(hashedEntry));

hashedEntry = pwdh.toHashedEntry(MD5_PASSWORD_3, CMD5_USERNAME_3);
assertTrue(MD5_HASHED_ENTRY_3.equalsIgnoreCase(hashedEntry));


hashedEntry = pwdh.toHashedEntry("badpassword", "charlie");
assertFalse(MD5_HASHED_ENTRY_1.equalsIgnoreCase(hashedEntry));

hashedEntry = pwdh.toHashedEntry("badpassword", null);
assertFalse(MD5_HASHED_ENTRY_1.equalsIgnoreCase(hashedEntry));
}

@Test(expected = IllegalArgumentException.class)
public void testToHashedEntryMD5NullPassword() {
PasswordHash pwdh = PasswordHash.instanceOf("MD5");
pwdh.toHashedEntry((String)null, null);
}


/**
* Test creating a hashed entry for scheme Combined-MD5. In this scheme there is no salt, so a direct
* comparison to a constant value is possible.
*
* Test method for {@link PasswordHash#toHashedEntry(String, String)} for CMD5.
*/
@Test
public void testToHashedEntryCMD5() {
PasswordHash pwdh = PasswordHash.instanceOf("CMD5");
String hashedEntry = pwdh.toHashedEntry(CMD5_PASSWORD_1, CMD5_USERNAME_1);
assertTrue(CMD5_HASHED_ENTRY_1.equalsIgnoreCase(hashedEntry));

hashedEntry = pwdh.toHashedEntry(CMD5_PASSWORD_2, CMD5_USERNAME_2);
assertTrue(CMD5_HASHED_ENTRY_2.equalsIgnoreCase(hashedEntry));

hashedEntry = pwdh.toHashedEntry(CMD5_PASSWORD_3, CMD5_USERNAME_3);
assertTrue(CMD5_HASHED_ENTRY_3.equalsIgnoreCase(hashedEntry));


hashedEntry = pwdh.toHashedEntry(CMD5_PASSWORD_1, "charlie");
assertFalse(CMD5_HASHED_ENTRY_1.equalsIgnoreCase(hashedEntry));

hashedEntry = pwdh.toHashedEntry("badpassword", "charlie");
assertFalse(MD5_HASHED_ENTRY_1.equalsIgnoreCase(hashedEntry));
}

@Test(expected = IllegalArgumentException.class)
public void testToHashedEntryCMD5NullPassword() {
PasswordHash pwdh = PasswordHash.instanceOf("CMD5");
pwdh.toHashedEntry((String)null, CMD5_USERNAME_1);
}

/**
* Test creating a hashed entry for scheme Combined-MD5, when no user is given.
* This should never happen in the application, so we expect an exception to be thrown.
*
* Test method for {@link PasswordHash#toHashedEntry(String, String)} for broken CMD5.
*/
@Test
public void testToHashedEntryCMD5NoUsername() {
PasswordHash pwdh = PasswordHash.instanceOf("CMD5");
try {
String hashedEntry = pwdh.toHashedEntry(CMD5_PASSWORD_1, "");
fail("CMD5 cannot work with an empty '' username. Got: " + hashedEntry);
}
catch (IllegalArgumentException ignored) { /*success*/ }

try {
String hashedEntry = pwdh.toHashedEntry(CMD5_PASSWORD_1, " ");
fail("CMD5 cannot work with an empty ' ' username. Got: " + hashedEntry);
}
catch (IllegalArgumentException ignored) { /*success*/ }

try {
String hashedEntry = pwdh.toHashedEntry(CMD5_PASSWORD_1, " ");
fail("CMD5 cannot work with an empty ' ' username. Got: " + hashedEntry);
}
catch (IllegalArgumentException ignored) { /*success*/ }

try {
String hashedEntry = pwdh.toHashedEntry(CMD5_PASSWORD_1, null);
fail("CMD5 cannot work with a null username. Got: " + hashedEntry);
}
catch (IllegalArgumentException ignored) { /*success*/ }
}

/**
* Test creating a hashed entry for scheme PBKDF2.
* Since this scheme uses a salt, we test by running a match. This is a bit backwards,
* but recreating the PBKDF2 here seems a little overkill.
*
* Test method for {@link PasswordHash#toHashedEntry(String, String)} for PBKDF2.
*/
@Test
public void testToHashedEntryPBKDF2() {
PasswordHash pwdh = PasswordHash.instanceOf("PBKDF2");
String hashedEntry = pwdh.toHashedEntry(PBKDF2_PASSWORD_1, null);
assertTrue("Type identifier is incorrect.", hashedEntry.startsWith(PasswordHash.Type.PBKDF2.name()));
PasswordHash pwdhverify = PasswordHash.instanceFor(hashedEntry);
assertNotNull(pwdhverify);
assertTrue(PBKDF2_PASSWORD_1, pwdhverify.matches(hashedEntry, PBKDF2_PASSWORD_1.toCharArray(), null));

hashedEntry = pwdh.toHashedEntry(PBKDF2_PASSWORD_2, "");
assertTrue("Type identifier is incorrect.", hashedEntry.startsWith(PasswordHash.Type.PBKDF2.name()));
pwdhverify = PasswordHash.instanceFor(hashedEntry);
assertNotNull(pwdhverify);
assertTrue(PBKDF2_PASSWORD_2, pwdhverify.matches(hashedEntry, PBKDF2_PASSWORD_2.toCharArray(), null));

hashedEntry = pwdh.toHashedEntry(PBKDF2_PASSWORD_0, "alpha");
assertTrue("Type identifier is incorrect.", hashedEntry.startsWith(PasswordHash.Type.PBKDF2.name()));
pwdhverify = PasswordHash.instanceFor(hashedEntry);
assertNotNull(pwdhverify);
assertTrue(PBKDF2_PASSWORD_0, pwdhverify.matches(hashedEntry, PBKDF2_PASSWORD_0.toCharArray(), null));
}

@Test(expected = IllegalArgumentException.class)
public void testToHashedEntryPBKDF2NullPassword() {
PasswordHash pwdh = PasswordHash.instanceOf("PBKDF2");
pwdh.toHashedEntry((String)null, null);
}


/**
* Test method for {@link com.gitblit.utils.PasswordHash#matches(String, char[], String)} for MD5.
*/
@Test
public void testMatchesMD5() {
PasswordHash pwdh = PasswordHash.instanceOf("MD5");

assertTrue("PWD0, Null user", pwdh.matches(MD5_HASHED_ENTRY_0, MD5_PASSWORD_0.toCharArray(), null));
assertTrue("PWD0, Empty user", pwdh.matches(MD5_HASHED_ENTRY_0, MD5_PASSWORD_0.toCharArray(), ""));
assertTrue("PWD0, With user", pwdh.matches(MD5_HASHED_ENTRY_0, MD5_PASSWORD_0.toCharArray(), "maxine"));

assertTrue("PWD1, Null user", pwdh.matches(MD5_HASHED_ENTRY_1, MD5_PASSWORD_1.toCharArray(), null));
assertTrue("PWD1, Empty user", pwdh.matches(MD5_HASHED_ENTRY_1, MD5_PASSWORD_1.toCharArray(), ""));
assertTrue("PWD1, With user", pwdh.matches(MD5_HASHED_ENTRY_1, MD5_PASSWORD_1.toCharArray(), "maxine"));

assertTrue("PWD2", pwdh.matches(MD5_HASHED_ENTRY_2, MD5_PASSWORD_2.toCharArray(), null));
assertTrue("PWD3", pwdh.matches(MD5_HASHED_ENTRY_3, MD5_PASSWORD_3.toCharArray(), null));


assertFalse("Matched wrong password", pwdh.matches(MD5_HASHED_ENTRY_1, "wrongpassword".toCharArray(), null));
assertFalse("Matched wrong password, with empty user", pwdh.matches(MD5_HASHED_ENTRY_1, "wrongpassword".toCharArray(), " "));
assertFalse("Matched wrong password, with user", pwdh.matches(MD5_HASHED_ENTRY_1, "wrongpassword".toCharArray(), "someuser"));

assertFalse("Matched empty password", pwdh.matches(MD5_HASHED_ENTRY_1, "".toCharArray(), null));
assertFalse("Matched empty password, with empty user", pwdh.matches(MD5_HASHED_ENTRY_1, " ".toCharArray(), " "));
assertFalse("Matched empty password, with user", pwdh.matches(MD5_HASHED_ENTRY_1, " ".toCharArray(), "someuser"));

assertFalse("Matched null password", pwdh.matches(MD5_HASHED_ENTRY_1, null, null));
assertFalse("Matched null password, with empty user", pwdh.matches(MD5_HASHED_ENTRY_1, null, " "));
assertFalse("Matched null password, with user", pwdh.matches(MD5_HASHED_ENTRY_1, null, "someuser"));


assertFalse("Matched wrong hashed entry", pwdh.matches(MD5_HASHED_ENTRY_1, MD5_PASSWORD_0.toCharArray(), null));
assertFalse("Matched wrong hashed entry, with empty user", pwdh.matches(MD5_HASHED_ENTRY_1, MD5_PASSWORD_0.toCharArray(), ""));
assertFalse("Matched wrong hashed entry, with user", pwdh.matches(MD5_HASHED_ENTRY_1, MD5_PASSWORD_0.toCharArray(), "someuser"));

assertFalse("Matched empty hashed entry", pwdh.matches("", MD5_PASSWORD_0.toCharArray(), null));
assertFalse("Matched empty hashed entry, with empty user", pwdh.matches(" ", MD5_PASSWORD_0.toCharArray(), ""));
assertFalse("Matched empty hashed entry, with user", pwdh.matches(" ", MD5_PASSWORD_0.toCharArray(), "someuser"));

assertFalse("Matched null entry", pwdh.matches(null, MD5_PASSWORD_0.toCharArray(), null));
assertFalse("Matched null entry, with empty user", pwdh.matches(null, MD5_PASSWORD_0.toCharArray(), ""));
assertFalse("Matched null entry, with user", pwdh.matches(null, MD5_PASSWORD_0.toCharArray(), "someuser"));


assertFalse("Matched wrong scheme", pwdh.matches(CMD5_HASHED_ENTRY_0, MD5_PASSWORD_0.toCharArray(), null));
assertFalse("Matched wrong scheme", pwdh.matches(PBKDF2_HASHED_ENTRY_0, MD5_PASSWORD_0.toCharArray(), ""));
assertFalse("Matched wrong scheme", pwdh.matches(CMD5_HASHED_ENTRY_0, CMD5_PASSWORD_0.toCharArray(), CMD5_USERNAME_0));
}

/**
* Test method for {@link com.gitblit.utils.PasswordHash#matches(String, char[], String)} for Combined-MD5.
*/
@Test
public void testMatchesCombinedMD5() {
PasswordHash pwdh = PasswordHash.instanceOf("CMD5");

assertTrue("PWD0", pwdh.matches(CMD5_HASHED_ENTRY_0, CMD5_PASSWORD_0.toCharArray(), CMD5_USERNAME_0));
assertTrue("PWD1", pwdh.matches(CMD5_HASHED_ENTRY_1, CMD5_PASSWORD_1.toCharArray(), CMD5_USERNAME_1));
assertTrue("PWD2", pwdh.matches(CMD5_HASHED_ENTRY_2, CMD5_PASSWORD_2.toCharArray(), CMD5_USERNAME_2));
assertTrue("PWD3", pwdh.matches(CMD5_HASHED_ENTRY_3, CMD5_PASSWORD_3.toCharArray(), CMD5_USERNAME_3));



assertFalse("Matched wrong password", pwdh.matches(CMD5_HASHED_ENTRY_1, "wrongpassword".toCharArray(), CMD5_USERNAME_1));
assertFalse("Matched wrong password", pwdh.matches(CMD5_HASHED_ENTRY_1, CMD5_PASSWORD_0.toCharArray(), CMD5_USERNAME_1));

assertFalse("Matched wrong user", pwdh.matches(CMD5_HASHED_ENTRY_1, CMD5_PASSWORD_1.toCharArray(), CMD5_USERNAME_0));
assertFalse("Matched wrong user", pwdh.matches(CMD5_HASHED_ENTRY_1, CMD5_PASSWORD_1.toCharArray(), "Samantha Jones"));

assertFalse("Matched empty user", pwdh.matches(CMD5_HASHED_ENTRY_1, CMD5_PASSWORD_1.toCharArray(), ""));
assertFalse("Matched empty user", pwdh.matches(CMD5_HASHED_ENTRY_1, CMD5_PASSWORD_1.toCharArray(), " "));
assertFalse("Matched null user", pwdh.matches(CMD5_HASHED_ENTRY_1, CMD5_PASSWORD_1.toCharArray(), null));

assertFalse("Matched empty hashed entry", pwdh.matches("", CMD5_PASSWORD_0.toCharArray(), CMD5_USERNAME_0));
assertFalse("Matched empty hashed entry, with empty user", pwdh.matches(" ", CMD5_PASSWORD_1.toCharArray(), ""));
assertFalse("Matched empty hashed entry, with null user", pwdh.matches(" ", CMD5_PASSWORD_0.toCharArray(), null));

assertFalse("Matched null entry, with user", pwdh.matches(null, CMD5_PASSWORD_1.toCharArray(), CMD5_USERNAME_1));
assertFalse("Matched null entry, with empty user", pwdh.matches(null, CMD5_PASSWORD_1.toCharArray(), ""));
assertFalse("Matched null entry, with null user", pwdh.matches(null, CMD5_PASSWORD_1.toCharArray(), null));


assertFalse("Matched wrong scheme", pwdh.matches(MD5_HASHED_ENTRY_0, CMD5_PASSWORD_0.toCharArray(), null));
assertFalse("Matched wrong scheme", pwdh.matches(PBKDF2_HASHED_ENTRY_0, CMD5_PASSWORD_0.toCharArray(), ""));
assertFalse("Matched wrong scheme", pwdh.matches(MD5_HASHED_ENTRY_0, CMD5_PASSWORD_0.toCharArray(), CMD5_USERNAME_0));
assertFalse("Matched wrong scheme", pwdh.matches(MD5_HASHED_ENTRY_0, MD5_PASSWORD_0.toCharArray(), CMD5_USERNAME_0));
}



/**
* Test method for {@link com.gitblit.utils.PasswordHash#matches(String, char[], String)} for PBKDF2.
*/
@Test
public void testMatchesPBKDF2() {
PasswordHash pwdh = PasswordHash.instanceOf("PBKDF2");

assertTrue("PWD0, Null user", pwdh.matches(PBKDF2_HASHED_ENTRY_0, PBKDF2_PASSWORD_0.toCharArray(), null));
assertTrue("PWD0, Empty user", pwdh.matches(PBKDF2_HASHED_ENTRY_0, PBKDF2_PASSWORD_0.toCharArray(), ""));
assertTrue("PWD0, With user", pwdh.matches(PBKDF2_HASHED_ENTRY_0, PBKDF2_PASSWORD_0.toCharArray(), "maxine"));

assertTrue("PWD1, Null user", pwdh.matches(PBKDF2_HASHED_ENTRY_1, PBKDF2_PASSWORD_1.toCharArray(), null));
assertTrue("PWD1, Empty user", pwdh.matches(PBKDF2_HASHED_ENTRY_1, PBKDF2_PASSWORD_1.toCharArray(), ""));
assertTrue("PWD1, With user", pwdh.matches(PBKDF2_HASHED_ENTRY_1, PBKDF2_PASSWORD_1.toCharArray(), "Maxim Gorki"));

assertTrue("PWD2, Null user", pwdh.matches(PBKDF2_HASHED_ENTRY_2, PBKDF2_PASSWORD_2.toCharArray(), null));
assertTrue("PWD2, Empty user", pwdh.matches(PBKDF2_HASHED_ENTRY_2, PBKDF2_PASSWORD_2.toCharArray(), ""));
assertTrue("PWD2, With user", pwdh.matches(PBKDF2_HASHED_ENTRY_2, PBKDF2_PASSWORD_2.toCharArray(), "Epson"));



assertFalse("Matched wrong password", pwdh.matches(PBKDF2_HASHED_ENTRY_1, "wrongpassword".toCharArray(), null));
assertFalse("Matched wrong password, with empty user", pwdh.matches(PBKDF2_HASHED_ENTRY_1, "wrongpassword".toCharArray(), " "));
assertFalse("Matched wrong password, with user", pwdh.matches(PBKDF2_HASHED_ENTRY_1, "wrongpassword".toCharArray(), "someuser"));

assertFalse("Matched empty password", pwdh.matches(PBKDF2_HASHED_ENTRY_2, "".toCharArray(), null));
assertFalse("Matched empty password, with empty user", pwdh.matches(PBKDF2_HASHED_ENTRY_2, " ".toCharArray(), " "));
assertFalse("Matched empty password, with user", pwdh.matches(PBKDF2_HASHED_ENTRY_2, " ".toCharArray(), "someuser"));

assertFalse("Matched null password", pwdh.matches(PBKDF2_HASHED_ENTRY_0, null, null));
assertFalse("Matched null password, with empty user", pwdh.matches(PBKDF2_HASHED_ENTRY_0, null, " "));
assertFalse("Matched null password, with user", pwdh.matches(PBKDF2_HASHED_ENTRY_0, null, "someuser"));


assertFalse("Matched wrong hashed entry", pwdh.matches(PBKDF2_HASHED_ENTRY_1, PBKDF2_PASSWORD_0.toCharArray(), null));
assertFalse("Matched wrong hashed entry, with empty user", pwdh.matches(PBKDF2_HASHED_ENTRY_1, PBKDF2_PASSWORD_0.toCharArray(), ""));
assertFalse("Matched wrong hashed entry, with user", pwdh.matches(PBKDF2_HASHED_ENTRY_1, PBKDF2_PASSWORD_0.toCharArray(), "someuser"));

assertFalse("Matched empty hashed entry", pwdh.matches("", PBKDF2_PASSWORD_0.toCharArray(), null));
assertFalse("Matched empty hashed entry, with empty user", pwdh.matches(" ", PBKDF2_PASSWORD_0.toCharArray(), ""));
assertFalse("Matched empty hashed entry, with user", pwdh.matches(" ", PBKDF2_PASSWORD_0.toCharArray(), "someuser"));

assertFalse("Matched null entry", pwdh.matches(null, PBKDF2_PASSWORD_0.toCharArray(), null));
assertFalse("Matched null entry, with empty user", pwdh.matches(null, PBKDF2_PASSWORD_0.toCharArray(), ""));
assertFalse("Matched null entry, with user", pwdh.matches(null, PBKDF2_PASSWORD_0.toCharArray(), "someuser"));


assertFalse("Matched wrong scheme", pwdh.matches(CMD5_HASHED_ENTRY_0, PBKDF2_PASSWORD_0.toCharArray(), null));
assertFalse("Matched wrong scheme", pwdh.matches(MD5_HASHED_ENTRY_0, PBKDF2_PASSWORD_0.toCharArray(), ""));
assertFalse("Matched wrong scheme", pwdh.matches(CMD5_HASHED_ENTRY_0, PBKDF2_PASSWORD_0.toCharArray(), CMD5_USERNAME_0));
}


/**
* Test method for {@link com.gitblit.utils.PasswordHash#matches(String, char[], String)}
* for old existing entries with the "PBKDF2WITHHMACSHA256" type identifier.
*/
@Test
public void testMatchesPBKDF2Compat() {
PasswordHash pwdh = PasswordHash.instanceOf("PBKDF2");

assertTrue("PWD3, Null user", pwdh.matches(PBKDF2_HASHED_ENTRY_3, PBKDF2_PASSWORD_3.toCharArray(), null));
assertTrue("PWD3, Empty user", pwdh.matches(PBKDF2_HASHED_ENTRY_3, PBKDF2_PASSWORD_3.toCharArray(), ""));
assertTrue("PWD3, With user", pwdh.matches(PBKDF2_HASHED_ENTRY_3, PBKDF2_PASSWORD_3.toCharArray(), "maxine"));


assertFalse("Matched wrong password", pwdh.matches(PBKDF2_HASHED_ENTRY_3, "bar".toCharArray(), null));
assertFalse("Matched wrong password, with empty user", pwdh.matches(PBKDF2_HASHED_ENTRY_3, "bar".toCharArray(), " "));
assertFalse("Matched wrong password, with user", pwdh.matches(PBKDF2_HASHED_ENTRY_3, "bar".toCharArray(), "someuser"));

assertFalse("Matched empty password", pwdh.matches(PBKDF2_HASHED_ENTRY_3, "".toCharArray(), null));
assertFalse("Matched empty password, with empty user", pwdh.matches(PBKDF2_HASHED_ENTRY_3, " ".toCharArray(), " "));
assertFalse("Matched empty password, with user", pwdh.matches(PBKDF2_HASHED_ENTRY_3, " ".toCharArray(), "someuser"));

assertFalse("Matched null password", pwdh.matches(PBKDF2_HASHED_ENTRY_3, null, null));
assertFalse("Matched null password, with empty user", pwdh.matches(PBKDF2_HASHED_ENTRY_3, null, " "));
assertFalse("Matched null password, with user", pwdh.matches(PBKDF2_HASHED_ENTRY_3, null, "someuser"));


assertFalse("Matched wrong hashed entry", pwdh.matches(PBKDF2_HASHED_ENTRY_3, PBKDF2_PASSWORD_0.toCharArray(), null));
assertFalse("Matched wrong hashed entry, with empty user", pwdh.matches(PBKDF2_HASHED_ENTRY_3, PBKDF2_PASSWORD_0.toCharArray(), ""));
assertFalse("Matched wrong hashed entry, with user", pwdh.matches(PBKDF2_HASHED_ENTRY_3, PBKDF2_PASSWORD_0.toCharArray(), "someuser"));
}

@Test
public void getEntryType() {
assertEquals(PasswordHash.Type.MD5, PasswordHash.getEntryType("MD5:blah"));
assertEquals(PasswordHash.Type.MD5, PasswordHash.getEntryType("md5:blah"));
assertEquals(PasswordHash.Type.MD5, PasswordHash.getEntryType("mD5:blah"));

assertEquals(PasswordHash.Type.CMD5, PasswordHash.getEntryType("CMD5:blah"));
assertEquals(PasswordHash.Type.CMD5, PasswordHash.getEntryType("cmd5:blah"));
assertEquals(PasswordHash.Type.CMD5, PasswordHash.getEntryType("Cmd5:blah"));

assertEquals(PasswordHash.Type.CMD5, PasswordHash.getEntryType("combined-md5:blah"));
assertEquals(PasswordHash.Type.CMD5, PasswordHash.getEntryType("COMBINED-MD5:blah"));
assertEquals(PasswordHash.Type.CMD5, PasswordHash.getEntryType("combined-MD5:blah"));

assertEquals(PasswordHash.Type.PBKDF2, PasswordHash.getEntryType("PBKDF2:blah"));
assertEquals(PasswordHash.Type.PBKDF2, PasswordHash.getEntryType("pbkdf2:blah"));
assertEquals(PasswordHash.Type.PBKDF2, PasswordHash.getEntryType("Pbkdf2:blah"));
assertEquals(PasswordHash.Type.PBKDF2, PasswordHash.getEntryType("pbKDF2:blah"));

assertEquals(PasswordHash.Type.PBKDF2, PasswordHash.getEntryType("PBKDF2WithHmacSHA256:blah"));
assertEquals(PasswordHash.Type.PBKDF2, PasswordHash.getEntryType("PBKDF2WITHHMACSHA256:blah"));
}

@Test
public void getEntryValue() {
assertEquals("value", PasswordHash.getEntryValue("MD5:value"));
assertEquals("plain text", PasswordHash.getEntryValue("plain text"));
assertEquals("what this", PasswordHash.getEntryValue(":what this"));
assertEquals("", PasswordHash.getEntryValue(":"));
}
}

Loading…
Cancel
Save