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

@ -53,6 +53,11 @@
</build>
<dependencies>
<dependency>
<groupId>de.mkammerer</groupId>
<artifactId>argon2-jvm</artifactId>
<version>2.4</version>
</dependency>
<dependency>
<groupId>org.vivoweb</groupId>
<artifactId>vitro-dependencies</artifactId>

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,

View file

@ -81,7 +81,8 @@ public class UserAccountsSelectorTest extends AbstractTestClass {
assertEquals("email", "email@jones.edu", acct.getEmailAddress());
assertEquals("firstName", "Bob", acct.getFirstName());
assertEquals("lastName", "Caruso", acct.getLastName());
assertEquals("password", "garbage", acct.getMd5Password());
assertEquals("md5password", "garbage", acct.getMd5Password());
assertEquals("argon2password", "betterGarbage", acct.getArgon2Password());
assertEquals("expires", 1100234965897L, acct.getPasswordLinkExpires());
assertEquals("loginCount", 50, acct.getLoginCount());
assertEquals("lastLogin", 1020304050607080L, acct.getLastLoginTime());
@ -105,7 +106,8 @@ public class UserAccountsSelectorTest extends AbstractTestClass {
assertEquals("email", "email@henry.edu", acct.getEmailAddress());
assertEquals("firstName", "Mary", acct.getFirstName());
assertEquals("lastName", "McInerney", acct.getLastName());
assertEquals("password", "garbage", acct.getMd5Password());
assertEquals("md5password", "garbage", acct.getMd5Password());
assertEquals("argon2password", "betterGarbage", acct.getArgon2Password());
assertEquals("expires", 0L, acct.getPasswordLinkExpires());
assertEquals("loginCount", 7, acct.getLoginCount());
assertEquals("lastLogin", 1122334455667788L, acct.getLastLoginTime());

View file

@ -9,6 +9,8 @@ import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import de.mkammerer.argon2.Argon2;
import de.mkammerer.argon2.Argon2Factory;
import edu.cornell.mannlib.vedit.beans.LoginStatusBean;
import edu.cornell.mannlib.vedit.beans.LoginStatusBean.AuthenticationSource;
import edu.cornell.mannlib.vitro.webapp.beans.UserAccount;
@ -102,6 +104,49 @@ public class AuthenticatorStub extends Authenticator {
return true;
}
@Override
public boolean md5HashIsNull(UserAccount userAccount){
if(userAccount!=null) {
if (userAccount.getMd5Password().compareTo("") == 0 ||
userAccount.getMd5Password() == null)
return true;
else
return false;
}
return false;
}
/**
* Applies Argon2i hashing on a string.
* Used by tests only with pre-specified values because the configuration
* properties (runtime.properties) is not set at compile time.
**/
public static String applyArgon2iEncodingStub(String raw) {
Argon2 argon2 = Argon2Factory.create();
try {
return argon2.hash(200, 500, 1, raw);
} catch (Exception e) {
// This can't happen with a normal Java runtime.
throw new RuntimeException(e);
}
}
@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

@ -98,7 +98,8 @@ public class ProgramLoginTest extends AbstractTestClass {
user.setUri(uri);
user.setPermissionSetUris(Collections
.singleton(PermissionSets.URI_DBA));
user.setMd5Password(Authenticator.applyMd5Encoding(password));
user.setArgon2Password(AuthenticatorStub.applyArgon2iEncodingStub(password));
user.setMd5Password("");
user.setLoginCount(loginCount);
user.setPasswordChangeRequired(loginCount == 0);
return user;

View file

@ -191,7 +191,8 @@ public class AuthenticateTest extends AbstractTestClass {
user.setEmailAddress(userInfo.username);
user.setUri(userInfo.uri);
user.setPermissionSetUris(userInfo.permissionSetUris);
user.setMd5Password(Authenticator.applyMd5Encoding(userInfo.password));
user.setArgon2Password(AuthenticatorStub.applyArgon2iEncodingStub(userInfo.password));
user.setMd5Password("");
user.setLoginCount(userInfo.loginCount);
user.setPasswordChangeRequired(userInfo.loginCount == 0);
return user;

View file

@ -91,20 +91,20 @@ public class UserAccountsDaoJenaTest extends AbstractTestClass {
@Before
public void createUserAccountValues() {
user1 = userAccount(URI_USER1, "email@able.edu", "Zack", "Roberts",
"garbage", "", 0L, false, 5, 12345678L, Status.ACTIVE, "user1",
"garbage", "" ,"", 0L, false, 5, 12345678L, Status.ACTIVE, "user1",
false, collection(URI_ROLE1), false, EMPTY);
userNew = userAccount("", "email@here", "Joe", "Blow", "XXXX", "YYYY",
userNew = userAccount("", "email@here", "Joe", "Blow", "XXXX","", "YYYY",
0L, false, 1, 0L, Status.ACTIVE, "jblow", false, EMPTY, false,
EMPTY);
userA = userAccount("", "aahern@here", "Alf", "Ahern", "XXXX", "YYYY",
userA = userAccount("", "aahern@here", "Alf", "Ahern", "XXXX", "", "YYYY",
0L, false, 1, 0L, Status.ACTIVE, "aahern", false, EMPTY, false,
collection(URI_PROFILE1));
userB = userAccount("", "email@here", "Betty", "Boop", "XXXX", "YYYY",
userB = userAccount("", "email@here", "Betty", "Boop", "XXXX", "", "YYYY",
0L, false, 1, 0L, Status.ACTIVE, "bboop", false, EMPTY, false,
collection(URI_PROFILE1, URI_PROFILE2));
userC = userAccount("", "ccallas@here", "Charlie", "Callas", "XXXX",
"YYYY", 0L, false, 1, 0L, Status.ACTIVE, "ccallas", false,
userC = userAccount("", "ccallas@here", "Charlie", "Callas", "XXXX", "",
"YYYY", 0L, false, 1, 0L, Status.ACTIVE, "ccallas", false,
EMPTY, false, collection(URI_PROFILE2));
}
@ -179,7 +179,7 @@ public class UserAccountsDaoJenaTest extends AbstractTestClass {
@Test
public void updateUserAccountSuccess() {
UserAccount orig = userAccount(URI_USER1, "updatedEmail@able.edu",
"Ezekiel", "Roberts", "differentHash", "oldHash", 1L, false,
"Ezekiel", "Roberts", "differentHash", "", "oldHash", 1L, false,
43, 1020304050607080L, Status.ACTIVE, "updatedUser1", false,
collection(URI_ROLE1, URI_ROLE3), false, EMPTY);
@ -379,7 +379,7 @@ public class UserAccountsDaoJenaTest extends AbstractTestClass {
}
private UserAccount userAccount(String uri, String emailAddress,
String firstName, String lastName, String md5Password,
String firstName, String lastName, String argon2Password, String md5Password,
String oldPassword, long passwordLinkExpires,
boolean passwordChangeRequired, int loginCount, long lastLoginTime,
Status status, String externalAuthId, boolean externalAuthOnly,
@ -390,6 +390,7 @@ public class UserAccountsDaoJenaTest extends AbstractTestClass {
ua.setEmailAddress(emailAddress);
ua.setFirstName(firstName);
ua.setLastName(lastName);
ua.setArgon2Password(argon2Password);
ua.setMd5Password(md5Password);
ua.setOldPassword(oldPassword);
ua.setPasswordLinkExpires(passwordLinkExpires);
@ -411,6 +412,7 @@ public class UserAccountsDaoJenaTest extends AbstractTestClass {
out.setEmailAddress(in.getEmailAddress());
out.setFirstName(in.getFirstName());
out.setLastName(in.getLastName());
out.setArgon2Password(in.getArgon2Password());
out.setMd5Password(in.getMd5Password());
out.setOldPassword(in.getOldPassword());
out.setPasswordLinkExpires(in.getPasswordLinkExpires());
@ -433,7 +435,7 @@ public class UserAccountsDaoJenaTest extends AbstractTestClass {
assertEquals("email", e.getEmailAddress(), a.getEmailAddress());
assertEquals("first name", e.getFirstName(), a.getFirstName());
assertEquals("last name", e.getLastName(), a.getLastName());
assertEquals("password", e.getMd5Password(), a.getMd5Password());
assertEquals("password", e.getArgon2Password(), e.getArgon2Password());
assertEquals("old password", e.getOldPassword(), a.getOldPassword());
assertEquals("link expires", e.getPasswordLinkExpires(),
a.getPasswordLinkExpires());

View file

@ -113,6 +113,7 @@ mydomain:user08
auth:firstName "Mary" ;
auth:lastName "McInerney" ;
auth:md5password "garbage" ;
auth:argon2password "betterGarbage" ;
auth:passwordChangeExpires 0 ;
auth:loginCount 7 ;
auth:lastLoginTime 1122334455667788 ;
@ -138,6 +139,7 @@ mydomain:user10
auth:firstName "Bob" ;
auth:lastName "Caruso" ;
auth:md5password "garbage" ;
auth:argon2password "betterGarbage" ;
auth:passwordChangeExpires 1100234965897 ;
auth:loginCount 50 ;
auth:lastLoginTime 1020304050607080 ;

View file

@ -0,0 +1,155 @@
# -----------------------------------------------------------------------------
#
# Vitro runtime properties
#
# This file is provided as example.runtime.properties.
#
# Save a copy of this file as runtime.properties in your Vitro home directory,
# and edit the properties as needed for your installation.
#
# -----------------------------------------------------------------------------
#
# This namespace will be used when generating URIs for objects created in the
# editor. In order to serve linked data, the default namespace must be composed
# as follows (optional elements in parentheses):
#
# scheme + server_name (+ port) (+ servlet_context) + "/individual/"
#
# For example, Cornell's default namespace is:
#
# http://vivo.cornell.edu/individual/
#
Vitro.defaultNamespace = http://vivo.mydomain.edu/individual/
#
# URL of Solr context used in local Vitro search. This will usually consist of:
# scheme + server_name + port + vitro_webapp_name + "solr"
# In the standard installation, the Solr context will be on the same server as Vitro,
# and in the same Tomcat instance. The path will be the Vitro webapp.name (specified
# above) + "solr"
# Example:
# vitro.local.solr.url = http://localhost:8080/vitrosolr
vitro.local.solr.url = http://localhost:8080/vitrosolr
#
# Email parameters which VIVO can use to send mail. If these are left empty,
# the "Contact Us" form will be disabled and users will not be notified of
# changes to their accounts.
#
email.smtpHost = smtp.my.domain.edu
email.replyTo = vivoAdmin@my.domain.edu
#
# The basic parameters for a MySQL database connection. Change the end of the
# URL to reflect your database name (if it is not "vitro"). Change the username
# and password to match the authorized user you created in MySQL.
#
VitroConnection.DataSource.url = jdbc:mysql://localhost/vitro
VitroConnection.DataSource.username = vitroweb
VitroConnection.DataSource.password = vitrovitro
#
# The maximum number of active connections in the database connection pool.
# Increase this value to support a greater number of concurrent page requests.
#
VitroConnection.DataSource.pool.maxActive = 40
#
# The maximum number of database connections that will be allowed
# to remain idle in the connection pool. Default is 25%
# of the maximum number of active connections.
#
VitroConnection.DataSource.pool.maxIdle = 10
#
# Parameters to change in order to use VIVO with a database other than
# MySQL.
#
VitroConnection.DataSource.dbtype = MySQL
VitroConnection.DataSource.driver = com.mysql.jdbc.Driver
VitroConnection.DataSource.validationQuery = SELECT 1
#
# The email address of the root user for the VIVO application. The password
# for this user is initially set to "rootPassword", but you will be asked to
# change the password the first time you log in.
#
rootUser.emailAddress = root@myDomain.com
#
# Argon2 password hashing parameters for time, memory and parallelism required to
# compute a hash.
#
# A time cost defines the amount of computation realized and therefore the execution
# time, given in a number of iterations.
# A memory cost defines the memory usage, given in kibibytes
# A parallelism degree defines the number of parallel threads
# For determining the optimal values of the parameters for your setup please refer to
# the white paper section 9
# https://github.com/P-H-C/phc-winner-argon2/blob/master/argon2-specs.pdf
#
argon2.parallelism =1
argon2.memory = 1024
argon2.time = 1000
#
# How is a logged-in user associated with a particular Individual? One way is
# for the Individual to have a property whose value is the username of the user.
# This is the name of that property.
#
selfEditing.idMatchingProperty = http://vitro.mydomain.edu/ns#networkId
#
# If an external authentication system like Shibboleth or CUWebAuth is to be
# used, these properties say how the login button should be labeled, and which
# HTTP header will contain the user ID from the authentication system. If such
# as system is not to be used, leave these commented out. Consult the
# installation instructions for more details.
#
#externalAuth.buttonText = Log in using BearCat Shibboleth
#externalAuth.netIdHeaderName = remote_userID
#
# Types of individual for which we can create proxy editors.
# If this is omitted, defaults to http://www.w3.org/2002/07/owl#Thing
proxy.eligibleTypeList = http://www.w3.org/2002/07/owl#Thing
#
# Show only the most appropriate data values based on the Accept-Language
# header supplied by the browser. Default is false if not set.
#
# RDFService.languageFilter = true
#
# Tell VIVO to generate HTTP headers on its responses to facilitate caching the
# profile pages that it creates.
#
# For more information, see
# https://wiki.duraspace.org/display/VIVO/Use+HTTP+caching+to+improve+performance
#
# Developers will likely want to leave caching disabled, since a change to a
# Freemarker template or to a Java class would not cause the page to be
# considered stale.
#
# http.createCacheHeaders = true
#
# Force VIVO to use a specific language or Locale instead of those
# specified by the browser. This affects RDF data retrieved from the model,
# if RDFService.languageFilter is true. This also affects the text of pages
# that have been modified to support multiple languages.
#
# languages.forceLocale = en_US
#
# A list of supported languages or Locales that the user may choose to
# use instead of the one specified by the browser. Selection images must
# be available in the i18n/images directory of the theme. This affects
# RDF data retrieved from the model, if RDFService.languageFilter is true.
# This also affects the text of pages that have been modified to support
# multiple languages.
#
# This should not be used with languages.forceLocale, which will override it.
#
# languages.selectableLocales = en, es, fr

View file

@ -77,6 +77,22 @@ VitroConnection.DataSource.validationQuery = SELECT 1
#
rootUser.emailAddress = root@myDomain.com
#
# Argon2 password hashing parameters for time, memory and parallelism required to
# compute a hash.
#
# A time cost defines the amount of computation realized and therefore the execution
# time, given in a number of iterations.
# A memory cost defines the memory usage, given in kibibytes
# A parallelism degree defines the number of parallel threads
# For determining the optimal values of the parameters for your setup please refer to
# the white paper section 9
# https://github.com/P-H-C/phc-winner-argon2/blob/master/argon2-specs.pdf
#
argon2.parallelism =1
argon2.memory = 1024
argon2.time = 1000
#
# How is a logged-in user associated with a particular Individual? One way is
# for the Individual to have a property whose value is the username of the user.

View file

@ -123,6 +123,8 @@ error_no_new_password = Please enter your new password.
error_passwords_dont_match = The passwords entered do not match.
error_password_length = Please enter a password between {0} and {1} characters in length.
error_previous_password = Your new password cannot match the current one.
password_system_has_changed = Our password system has been upgraded to make it more secure. \
Please enter a new password.
search_accounts_button = Search accounts
accounts_search_results = Search results for