Merge pull request #73 from Asimq/develop

Moved password encryption from MD5 to a salted and secure hash - 1448
This commit is contained in:
Mike Conlon 2018-05-23 13:49:04 -04:00 committed by GitHub
commit 111c0a8ee5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 487 additions and 50 deletions

View file

@ -102,7 +102,7 @@ public class PolicyHelper {
String uri = user.getUri();
log.debug("userAccount is '" + uri + "'");
if (!auth.isCurrentPassword(user, password)) {
if (!auth.isCurrentPasswordArgon2(user, password)) {
log.debug(String.format("UNAUTHORIZED, password not accepted "
+ "for %s, account URI: %s", email, uri));
return false;

View file

@ -67,6 +67,7 @@ public class RootUserPolicy implements PolicyIface {
private ServletContext ctx;
private StartupStatus ss;
private UserAccountsDao uaDao;
private ConfigurationProperties cp;
private String configuredRootUser;
private boolean configuredRootUserExists;
private TreeSet<String> otherRootUsers;
@ -75,6 +76,7 @@ public class RootUserPolicy implements PolicyIface {
public void contextInitialized(ServletContextEvent sce) {
ctx = sce.getServletContext();
ss = StartupStatus.getBean(ctx);
cp = ConfigurationProperties.getBean(ctx);
try {
uaDao = ModelAccess.on(ctx).getWebappDaoFactory()
@ -148,8 +150,9 @@ public class RootUserPolicy implements PolicyIface {
ua.setEmailAddress(configuredRootUser);
ua.setFirstName("root");
ua.setLastName("user");
ua.setMd5Password(Authenticator
.applyMd5Encoding(ROOT_USER_INITIAL_PASSWORD));
ua.setArgon2Password(Authenticator.applyArgon2iEncoding(
ROOT_USER_INITIAL_PASSWORD));
ua.setMd5Password("");
ua.setPasswordChangeRequired(true);
ua.setStatus(Status.ACTIVE);
ua.setRootUser(true);

View file

@ -48,6 +48,7 @@ public class UserAccount {
private String firstName = ""; // Never null.
private String lastName = ""; // Never null.
private String argon2Password = ""; //Never null.
private String md5Password = ""; // Never null.
private String oldPassword = ""; // Never null.
private long passwordLinkExpires = 0L; // Never negative.
@ -104,6 +105,14 @@ public class UserAccount {
this.lastName = nonNull(lastName, "");
}
public String getArgon2Password() {
return argon2Password;
}
public void setArgon2Password(String argo2Password) {
this.argon2Password = nonNull(argo2Password, "");
}
public String getMd5Password() {
return md5Password;
}
@ -125,7 +134,7 @@ public class UserAccount {
}
public String getPasswordLinkExpiresHash() {
return limitStringLength(8, Authenticator.applyMd5Encoding(String
return limitStringLength(8, Authenticator.applyArgon2iEncoding(String
.valueOf(passwordLinkExpires)));
}
@ -236,6 +245,7 @@ public class UserAccount {
+ (", firstName=" + firstName) + (", lastName=" + lastName)
+ (", md5password=" + md5Password)
+ (", oldPassword=" + oldPassword)
+ (", argon2password=" + argon2Password)
+ (", passwordLinkExpires=" + passwordLinkExpires)
+ (", passwordChangeRequired=" + passwordChangeRequired)
+ (", externalAuthOnly=" + externalAuthOnly)

View file

@ -29,6 +29,9 @@ public class ConfigurationPropertiesSmokeTests implements
private static final String PROPERTY_LANGUAGE_SELECTABLE = "languages.selectableLocales";
private static final String PROPERTY_LANGUAGE_FORCE = "languages.forceLocale";
private static final String PROPERTY_LANGUAGE_FILTER = "RDFService.languageFilter";
private static final String PROPERTY_ARGON2_TIME = "argon2.time";
private static final String PROPERTY_ARGON2_MEMORY = "argon2.memory";
private static final String PROPERTY_ARGON2_PARALLELISM = "argon2.parallelism";
@Override
public void contextInitialized(ServletContextEvent sce) {
@ -39,6 +42,7 @@ public class ConfigurationPropertiesSmokeTests implements
checkDefaultNamespace(ctx, props, ss);
checkMultipleRPFs(ctx, props, ss);
checkLanguages(props, ss);
checkEncryptionParameters(props, ss);
}
/**
@ -149,6 +153,26 @@ public class ConfigurationPropertiesSmokeTests implements
}
}
/**
* Fail if there are no config properties for the Argon2 encryption.
*/
private void checkEncryptionParameters(ConfigurationProperties props,
StartupStatus ss) {
failIfNotPresent(props, ss, PROPERTY_ARGON2_TIME);
failIfNotPresent(props, ss, PROPERTY_ARGON2_MEMORY);
failIfNotPresent(props, ss, PROPERTY_ARGON2_PARALLELISM);
}
private void failIfNotPresent(ConfigurationProperties props,
StartupStatus ss, String name) {
String value = props.getProperty(name);
if (value == null || value.isEmpty()) {
ss.fatal(this, "runtime.properties does not contain a value for '"
+ name + "'");
return;
}
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
// nothing to do at shutdown

View file

@ -37,7 +37,7 @@ public class UserAccountsSelector {
+ "PREFIX auth: <http://vitro.mannlib.cornell.edu/ns/vitro/authorization#> \n";
private static final String ALL_VARIABLES = "?uri ?email ?firstName "
+ "?lastName ?pwd ?expire ?count ?lastLogin ?status ?isRoot";
+ "?lastName ?md5pwd ?a2pwd ?expire ?count ?lastLogin ?status ?isRoot";
private static final String COUNT_VARIABLE = "?uri";
@ -158,7 +158,8 @@ public class UserAccountsSelector {
private String optionalClauses() {
return "OPTIONAL { ?uri auth:firstName ?firstName } \n"
+ " OPTIONAL { ?uri auth:lastName ?lastName } \n"
+ " OPTIONAL { ?uri auth:md5password ?pwd } \n"
+ " OPTIONAL { ?uri auth:md5password ?md5pwd } \n"
+ " OPTIONAL { ?uri auth:argon2password ?a2pwd } \n"
+ " OPTIONAL { ?uri auth:passwordChangeExpires ?expire } \n"
+ " OPTIONAL { ?uri auth:loginCount ?count } \n"
+ " OPTIONAL { ?uri auth:lastLoginTime ?lastLogin } \n"
@ -245,7 +246,8 @@ public class UserAccountsSelector {
user.setEmailAddress(solution.getLiteral("email").getString());
user.setFirstName(ifLiteralPresent(solution, "firstName", ""));
user.setLastName(ifLiteralPresent(solution, "lastName", ""));
user.setMd5Password(ifLiteralPresent(solution, "pwd", ""));
user.setMd5Password(ifLiteralPresent(solution, "md5pwd", ""));
user.setArgon2Password(ifLiteralPresent(solution, "a2pwd", ""));
user.setPasswordLinkExpires(ifLongPresent(solution, "expire", 0L));
user.setLoginCount(ifIntPresent(solution, "count", 0));
user.setLastLoginTime(ifLongPresent(solution, "lastLogin", 0));

View file

@ -198,8 +198,8 @@ public abstract class UserAccountsAddPageStrategy extends UserAccountsPage {
@Override
protected void setAdditionalProperties(UserAccount u) {
if (!page.isExternalAuthOnly()) {
u.setMd5Password(Authenticator
.applyMd5Encoding(initialPassword));
u.setArgon2Password(Authenticator.applyArgon2iEncoding(initialPassword));
u.setMd5Password("");
u.setPasswordChangeRequired(true);
}
u.setStatus(Status.ACTIVE);

View file

@ -194,7 +194,8 @@ public abstract class UserAccountsEditPageStrategy extends UserAccountsPage {
@Override
protected void setAdditionalProperties(UserAccount u) {
if (!page.isExternalAuthOnly() && !newPassword.isEmpty()) {
u.setMd5Password(Authenticator.applyMd5Encoding(newPassword));
u.setArgon2Password(Authenticator.applyArgon2iEncoding(newPassword));
u.setMd5Password("");
u.setPasswordChangeRequired(true);
}
}

View file

@ -33,14 +33,14 @@ public class UserAccountsCreatePasswordPage extends
}
public void createPassword() {
userAccount.setMd5Password(Authenticator.applyMd5Encoding(newPassword));
userAccount.setArgon2Password(Authenticator.applyArgon2iEncoding(newPassword));
userAccount.setMd5Password("");
userAccount.setPasswordLinkExpires(0L);
userAccount.setPasswordChangeRequired(false);
userAccount.setStatus(Status.ACTIVE);
userAccountsDao.updateUserAccount(userAccount);
log.debug("Set password on '" + userAccount.getEmailAddress()
+ "' to '" + newPassword + "'");
notifyUser();
}

View file

@ -155,8 +155,8 @@ public abstract class UserAccountsMyAccountPageStrategy extends
@Override
public void setAdditionalProperties(UserAccount userAccount) {
if (!newPassword.isEmpty() && !page.isExternalAuthOnly()) {
userAccount.setMd5Password(Authenticator
.applyMd5Encoding(newPassword));
userAccount.setArgon2Password(Authenticator.applyArgon2iEncoding(newPassword));
userAccount.setMd5Password("");
userAccount.setPasswordChangeRequired(false);
userAccount.setPasswordLinkExpires(0L);
}

View file

@ -33,7 +33,8 @@ public class UserAccountsResetPasswordPage extends UserAccountsPasswordBasePage
}
public void resetPassword() {
userAccount.setMd5Password(Authenticator.applyMd5Encoding(newPassword));
userAccount.setArgon2Password(Authenticator.applyArgon2iEncoding(newPassword));
userAccount.setMd5Password("");
userAccount.setPasswordLinkExpires(0L);
userAccount.setPasswordChangeRequired(false);
userAccount.setStatus(Status.ACTIVE);

View file

@ -54,7 +54,7 @@ public class VitroApiServlet extends HttpServlet {
+ "last names and a valid email address.");
}
if (!auth.isCurrentPassword(account, password)) {
if (!auth.isCurrentPasswordArgon2(account, password)) {
log.debug("Invalid: '" + email + "'/'" + password + "'");
throw new AuthException("email/password combination is not valid");
}

View file

@ -141,8 +141,12 @@ public class AdminLoginController extends FreemarkerHttpServlet {
}
private boolean newPasswordRequired() {
return auth.isCurrentPassword(userAccount, password)
&& (userAccount.isPasswordChangeRequired());
if(auth.md5HashIsNull(userAccount)) {
return auth.isCurrentPasswordArgon2(userAccount, password)
&& userAccount.isPasswordChangeRequired();
}
else
return auth.isCurrentPassword(userAccount, password); // MD5 password should be changed anyway
}
private boolean isPasswordValidLength(String pw) {
@ -151,8 +155,18 @@ public class AdminLoginController extends FreemarkerHttpServlet {
}
private boolean tryToLogin() {
if (!auth.isCurrentPassword(userAccount, password)) {
return false;
if(auth.md5HashIsNull(userAccount)) {
if (!auth.isCurrentPasswordArgon2(userAccount, password))
return false;
}
else {
if (!auth.isCurrentPassword(userAccount, password))
return false;
else {
userAccount.setPasswordChangeRequired(true);
userAccount.setMd5Password("");
}
}
try {

View file

@ -2,21 +2,23 @@
package edu.cornell.mannlib.vitro.webapp.controller.authenticate;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.List;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.codec.binary.Hex;
import de.mkammerer.argon2.Argon2;
import de.mkammerer.argon2.Argon2Factory;
import edu.cornell.mannlib.vedit.beans.LoginStatusBean.AuthenticationSource;
import edu.cornell.mannlib.vitro.webapp.auth.identifier.ActiveIdentifierBundleFactories;
import edu.cornell.mannlib.vitro.webapp.auth.identifier.IdentifierBundle;
import edu.cornell.mannlib.vitro.webapp.beans.UserAccount;
import edu.cornell.mannlib.vitro.webapp.config.ConfigurationProperties;
import org.apache.commons.codec.binary.Hex;
import edu.cornell.mannlib.vitro.webapp.application.ApplicationUtils;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.List;
/**
* The tool that a login process will use to interface with the user records in
@ -55,6 +57,7 @@ public abstract class Authenticator {
*
* If there is no factory, configure a Basic one.
*/
public static Authenticator getInstance(HttpServletRequest request) {
ServletContext ctx = request.getSession().getServletContext();
Object attribute = ctx.getAttribute(FACTORY_ATTRIBUTE_NAME);
@ -112,6 +115,22 @@ public abstract class Authenticator {
public abstract boolean isCurrentPassword(UserAccount userAccount,
String clearTextPassword);
/**
* Does this UserAccount have this Argon2 password? False if the
* userAccount is null.
*/
public abstract boolean isCurrentPasswordArgon2(UserAccount userAccount,
String clearTextPassword);
/**
*
* Checks if the user still has got an MD5 Password
*/
public abstract boolean md5HashIsNull(UserAccount userAccount);
/**
* Internal: record a new password for the user. Takes no action if the
* userAccount is null.
@ -180,6 +199,46 @@ public abstract class Authenticator {
}
}
/**
* Applies Argon2i hashing on a string. Obtains the argon2i parameters
* from the configuration properties specified in the runtime.properties
* through this class "Authenticator".
**/
public static String applyArgon2iEncoding(String raw) {
ServletContext ctx = ApplicationUtils.instance().getServletContext();
ConfigurationProperties configProp = ConfigurationProperties.getBean(ctx);
Argon2 argon2 = Argon2Factory.create();
if (configProp.getProperty("argon2.time") != null
&& configProp.getProperty("argon2.memory") != null
&& configProp.getProperty("argon2.parallelism") != null) {
return argon2.hash(
Integer.parseInt(configProp.getProperty("argon2.time")),
Integer.parseInt(configProp.getProperty("argon2.memory")),
Integer.parseInt(configProp.getProperty("argon2.parallelism")), raw);
} else {
throw new RuntimeException(
"Parameters \"argon2.time\", \"argon2.memory\" and "
+ "\"argon2.parallelism\" are either missing in the "
+ "\"runtime.properties\" file or are not defined correctly");
}
}
/**
Verifies the string against the Argon2i hash stored for a user account
*/
public static boolean verifyArgon2iHash(String hash, String raw)
{
Argon2 argon2 = Argon2Factory.create();
return argon2.verify(hash, raw);
}
/**
* Check whether the form of the emailAddress is syntactically correct. Does
* not allow multiple addresses. Does not allow local addresses (without a

View file

@ -98,6 +98,30 @@ public class BasicAuthenticator extends Authenticator {
return encodedPassword.equals(userAccount.getMd5Password());
}
@Override
public boolean md5HashIsNull(UserAccount userAccount){
if(userAccount.getMd5Password().compareTo("")==0 ||
userAccount.getMd5Password()==null)
return true;
else
return false;
}
@Override
public boolean isCurrentPasswordArgon2(UserAccount userAccount,
String clearTextPassword) {
if (userAccount == null) {
return false;
}
if (clearTextPassword == null) {
return false;
}
return verifyArgon2iHash(userAccount.getArgon2Password(),
clearTextPassword);
}
@Override
public void recordNewPassword(UserAccount userAccount,
String newClearTextPassword) {
@ -105,7 +129,9 @@ public class BasicAuthenticator extends Authenticator {
log.error("Trying to change password on null user.");
return;
}
userAccount.setMd5Password(applyMd5Encoding(newClearTextPassword));
userAccount.setArgon2Password((applyArgon2iEncoding(
newClearTextPassword)));
userAccount.setMd5Password("");
userAccount.setPasswordChangeRequired(false);
userAccount.setPasswordLinkExpires(0L);
getUserAccountsDao().updateUserAccount(userAccount);

View file

@ -5,6 +5,7 @@ package edu.cornell.mannlib.vitro.webapp.controller.authenticate;
import static edu.cornell.mannlib.vedit.beans.LoginStatusBean.AuthenticationSource.INTERNAL;
import static edu.cornell.mannlib.vitro.webapp.beans.UserAccount.MAX_PASSWORD_LENGTH;
import static edu.cornell.mannlib.vitro.webapp.beans.UserAccount.MIN_PASSWORD_LENGTH;
import static edu.cornell.mannlib.vitro.webapp.controller.login.LoginProcessBean.MLevel.ERROR;
import java.io.IOException;
import java.io.PrintWriter;
@ -158,7 +159,19 @@ public class ProgramLogin extends HttpServlet {
}
private boolean usernameAndPasswordAreValid() {
return auth.isCurrentPassword(userAccount, password);
if(auth.md5HashIsNull(userAccount)) {
if (!auth.isCurrentPasswordArgon2(userAccount, password))
return false;
}
else {
if (!auth.isCurrentPassword(userAccount, password))
return false;
else {
userAccount.setPasswordChangeRequired(true);
}
}
return true;
}
private boolean loginDisabled() {

View file

@ -76,6 +76,30 @@ public class RestrictedAuthenticator extends Authenticator {
return auth.getAccountForInternalAuth(emailAddress);
}
@Override
public boolean md5HashIsNull(UserAccount userAccount){
if(userAccount.getMd5Password().compareTo("")==0 ||
userAccount.getMd5Password()==null)
return true;
else
return false;
}
@Override
public boolean isCurrentPasswordArgon2(UserAccount userAccount,
String clearTextPassword) {
if (userAccount == null) {
return false;
}
if (clearTextPassword == null) {
return false;
}
return verifyArgon2iHash(userAccount.getArgon2Password(),
clearTextPassword);
}
@Override
public boolean isCurrentPassword(UserAccount userAccount,
String clearTextPassword) {

View file

@ -331,15 +331,37 @@ public class Authenticate extends VitroHttpServlet {
return;
}
if (!getAuthenticator(request).isUserPermittedToLogin(user)) {
bean.setMessage(request, ERROR, "logins_disabled_for_maintenance");
return;
}
if (!getAuthenticator(request).isCurrentPassword(user, password)) {
bean.setMessage(request, ERROR, "error_incorrect_credentials");
return;
if(getAuthenticator(request).md5HashIsNull(user)) {
if (!getAuthenticator(request)
.isCurrentPasswordArgon2(user, password)) {
bean.setMessage(request, ERROR,
"error_incorrect_credentials");
return;
}
}
else {
if (!getAuthenticator(request)
.isCurrentPassword(user, password)) {
bean.setMessage(request, ERROR,
"error_incorrect_credentials");
return;
}
else {
user.setPasswordChangeRequired(true);
user.setMd5Password("");
bean.setMessage(request, ERROR,
"password_system_has_changed");
}
}
// Username and password are correct. What next?
if (user.isPasswordChangeRequired()) {
@ -401,7 +423,7 @@ public class Authenticate extends VitroHttpServlet {
UserAccount user = getAuthenticator(request).getAccountForInternalAuth(
username);
if (getAuthenticator(request).isCurrentPassword(user, newPassword)) {
if (getAuthenticator(request).isCurrentPasswordArgon2(user, newPassword)) {
bean.setMessage(request, ERROR, "error_previous_password");
return;
}

View file

@ -149,6 +149,7 @@ public class VitroVocabulary {
public static final String USERACCOUNT_EMAIL_ADDRESS = VITRO_AUTH + "emailAddress";
public static final String USERACCOUNT_FIRST_NAME = VITRO_AUTH + "firstName";
public static final String USERACCOUNT_LAST_NAME = VITRO_AUTH + "lastName";
public static final String USERACCOUNT_ARGON2_PASSWORD = VITRO_AUTH + "argon2password";
public static final String USERACCOUNT_MD5_PASSWORD = VITRO_AUTH + "md5password";
public static final String USERACCOUNT_OLD_PASSWORD = VITRO_AUTH + "oldpassword";
public static final String USERACCOUNT_LOGIN_COUNT = VITRO_AUTH + "loginCount";

View file

@ -112,6 +112,7 @@ public class JenaBaseDaoCon {
protected DatatypeProperty USERACCOUNT_EMAIL_ADDRESS = _constModel.createDatatypeProperty(VitroVocabulary.USERACCOUNT_EMAIL_ADDRESS);
protected DatatypeProperty USERACCOUNT_FIRST_NAME = _constModel.createDatatypeProperty(VitroVocabulary.USERACCOUNT_FIRST_NAME);
protected DatatypeProperty USERACCOUNT_LAST_NAME = _constModel.createDatatypeProperty(VitroVocabulary.USERACCOUNT_LAST_NAME);
protected DatatypeProperty USERACCOUNT_ARGON2_PASSWORD = _constModel.createDatatypeProperty(VitroVocabulary.USERACCOUNT_ARGON2_PASSWORD);
protected DatatypeProperty USERACCOUNT_MD5_PASSWORD = _constModel.createDatatypeProperty(VitroVocabulary.USERACCOUNT_MD5_PASSWORD);
protected DatatypeProperty USERACCOUNT_OLD_PASSWORD = _constModel.createDatatypeProperty(VitroVocabulary.USERACCOUNT_OLD_PASSWORD);
protected DatatypeProperty USERACCOUNT_LOGIN_COUNT = _constModel.createDatatypeProperty(VitroVocabulary.USERACCOUNT_LOGIN_COUNT);

View file

@ -93,6 +93,7 @@ public class UserAccountsDaoJena extends JenaBaseDao implements UserAccountsDao
USERACCOUNT_EMAIL_ADDRESS));
u.setFirstName(getPropertyStringValue(r, USERACCOUNT_FIRST_NAME));
u.setLastName(getPropertyStringValue(r, USERACCOUNT_LAST_NAME));
u.setArgon2Password(getPropertyStringValue(r, USERACCOUNT_ARGON2_PASSWORD));
u.setMd5Password(getPropertyStringValue(r, USERACCOUNT_MD5_PASSWORD));
u.setOldPassword(getPropertyStringValue(r, USERACCOUNT_OLD_PASSWORD));
u.setPasswordLinkExpires(getPropertyLongValue(r,
@ -225,6 +226,8 @@ public class UserAccountsDaoJena extends JenaBaseDao implements UserAccountsDao
userAccount.getLastName(), model);
addPropertyStringValue(res, USERACCOUNT_MD5_PASSWORD,
userAccount.getMd5Password(), model);
addPropertyStringValue(res, USERACCOUNT_ARGON2_PASSWORD,
userAccount.getArgon2Password(), model);
addPropertyStringValue(res, USERACCOUNT_OLD_PASSWORD,
userAccount.getOldPassword(), model);
addPropertyLongValue(res, USERACCOUNT_PASSWORD_LINK_EXPIRES,
@ -288,6 +291,8 @@ public class UserAccountsDaoJena extends JenaBaseDao implements UserAccountsDao
userAccount.getLastName(), model);
updatePropertyStringValue(res, USERACCOUNT_MD5_PASSWORD,
userAccount.getMd5Password(), model);
updatePropertyStringValue(res, USERACCOUNT_ARGON2_PASSWORD,
userAccount.getArgon2Password(), model);
updatePropertyStringValue(res, USERACCOUNT_OLD_PASSWORD,
userAccount.getOldPassword(), model);
updatePropertyLongValue(res, USERACCOUNT_PASSWORD_LINK_EXPIRES,