You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jackrabbit.apache.org by an...@apache.org on 2012/08/07 20:21:12 UTC
svn commit: r1370420 - in /jackrabbit/trunk/jackrabbit-core/src:
main/java/org/apache/jackrabbit/core/security/authentication/
main/java/org/apache/jackrabbit/core/security/user/
main/java/org/apache/jackrabbit/core/security/user/action/ test/java/org/...
Author: angela
Date: Tue Aug 7 18:21:11 2012
New Revision: 1370420
URL: http://svn.apache.org/viewvc?rev=1370420&view=rev
Log:
JCR-3405 : Improvements to user management implementation
- password utility
- configurable password hashing: algorithm, number of iterations
- improve handling of plaintxt passwords starting with {algorithm}
Added:
jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/PasswordUtility.java
jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/PasswordUtilityTest.java
Modified:
jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authentication/CryptedSimpleCredentials.java
jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/UserImpl.java
jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/UserImporter.java
jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/UserManagerImpl.java
jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/action/PasswordValidationAction.java
jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/AuthorizableActionTest.java
jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/TestAll.java
jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/UserImplTest.java
Modified: jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authentication/CryptedSimpleCredentials.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authentication/CryptedSimpleCredentials.java?rev=1370420&r1=1370419&r2=1370420&view=diff
==============================================================================
--- jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authentication/CryptedSimpleCredentials.java (original)
+++ jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authentication/CryptedSimpleCredentials.java Tue Aug 7 18:21:11 2012
@@ -17,17 +17,14 @@
package org.apache.jackrabbit.core.security.authentication;
import org.apache.jackrabbit.core.security.SecurityConstants;
-import org.apache.jackrabbit.util.Text;
+import org.apache.jackrabbit.core.security.user.PasswordUtility;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.jcr.Credentials;
-import javax.jcr.RepositoryException;
import javax.jcr.SimpleCredentials;
import java.io.UnsupportedEncodingException;
import java.security.NoSuchAlgorithmException;
-import java.security.MessageDigest;
-import java.security.SecureRandom;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
@@ -39,9 +36,6 @@ public class CryptedSimpleCredentials im
private static final Logger log = LoggerFactory.getLogger(CryptedSimpleCredentials.class);
- private final String algorithm;
- private final String salt;
-
private final String hashedPassword;
private final String userId;
private final Map<String, Object> attributes;
@@ -68,9 +62,7 @@ public class CryptedSimpleCredentials im
throw new IllegalArgumentException();
}
String password = new String(pwd);
- algorithm = SecurityConstants.DEFAULT_DIGEST;
- salt = null; // backwards compatibility.
- hashedPassword = generateHash(password, algorithm, salt);
+ hashedPassword = PasswordUtility.buildPasswordHash(password);
String[] attNames = credentials.getAttributeNames();
attributes = new HashMap<String, Object>(attNames.length);
@@ -85,8 +77,7 @@ public class CryptedSimpleCredentials im
* In contrast to {@link CryptedSimpleCredentials(SimpleCredentials)} that
* expects the password to be plain text this constructor expects the
* password to be already crypted. However, it performs a simple validation
- * and calls {@link Text#digest} using the
- * {@link SecurityConstants#DEFAULT_DIGEST default digest} in case the
+ * and calls {@link PasswordUtility#buildPasswordHash(String)} in case the
* given password is found to be plain text.
*
* @param userId
@@ -102,17 +93,11 @@ public class CryptedSimpleCredentials im
throw new IllegalArgumentException("Password may not be null.");
}
this.userId = userId;
- String algo = extractAlgorithm(hashedPassword);
- if (algo == null) {
+ if (PasswordUtility.isPlainTextPassword(hashedPassword)) {
// password is plain text (including those starting with {invalidAlgorithm})
- log.debug("Plain text password -> Using " + SecurityConstants.DEFAULT_DIGEST + " to create digest.");
- algorithm = SecurityConstants.DEFAULT_DIGEST;
- salt = generateSalt();
- this.hashedPassword = generateHash(hashedPassword, algorithm, salt);
+ log.warn("Plain text password -> Using default algorithm to create digest.");
+ this.hashedPassword = PasswordUtility.buildPasswordHash(hashedPassword);
} else {
- // password is already hashed and started with {validAlgorithm}
- algorithm = algo;
- salt = extractSalt(hashedPassword, algorithm);
this.hashedPassword = hashedPassword;
}
attributes = Collections.emptyMap();
@@ -131,7 +116,7 @@ public class CryptedSimpleCredentials im
}
public String getAlgorithm() {
- return algorithm;
+ return PasswordUtility.extractAlgorithm(hashedPassword);
}
public String getPassword() {
@@ -162,114 +147,10 @@ public class CryptedSimpleCredentials im
if (getUserID().equalsIgnoreCase(credentials.getUserID())) {
// crypt the password retrieved from the given simple credentials
- // and test if it is equal to the cryptedPassword field.
- return hashedPassword.equals(generateHash(String.valueOf(credentials.getPassword()), algorithm, salt));
+ // and test if it is equal to the password hash defined with this
+ // CryptedSimpleCredentials instance.
+ return PasswordUtility.isSame(hashedPassword, String.valueOf(credentials.getPassword()));
}
return false;
}
-
- /**
- * Creates a hash of the specified password if it is found to be plain text.
- *
- * @param password
- * @return
- * @throws javax.jcr.RepositoryException
- */
- public static String buildPasswordHash(String password) throws RepositoryException {
- try {
- return new CryptedSimpleCredentials("_", password).getPassword();
- } catch (NoSuchAlgorithmException e) {
- throw new RepositoryException(e);
- } catch (UnsupportedEncodingException e) {
- throw new RepositoryException(e);
- }
- }
-
- /**
- * @param pwd Plain text password
- * @param algorithm The algorithm to be used for the digest.
- * @param salt The salt to be used for the digest.
- * @return Digest of the given password with leading algorithm and optionally
- * salt information.
- * @throws NoSuchAlgorithmException
- * @throws UnsupportedEncodingException
- */
- private static String generateHash(String pwd, String algorithm, String salt)
- throws NoSuchAlgorithmException, UnsupportedEncodingException {
-
- StringBuilder password = new StringBuilder();
- password.append("{").append(algorithm).append("}");
- if (salt != null && salt.length() > 0) {
- password.append(salt).append("-");
- StringBuilder data = new StringBuilder();
- data.append(salt).append(pwd);
- password.append(Text.digest(algorithm, data.toString().getBytes("UTF-8")));
- } else {
- password.append(Text.digest(algorithm, pwd.getBytes("UTF-8")));
- }
- return password.toString();
- }
-
- /**
- * Extract the algorithm from the given crypted password string. Returns the
- * algorithm or <code>null</code> if the given string doesn't have a
- * leading <code>{algorithm}</code> such as created by {@link #generateHash(String, String, String)
- * or if the extracted string doesn't represent an available algorithm.
- *
- * @param hashedPwd
- * @return The algorithm or <code>null</code> if the given string doesn't have a
- * leading <code>{algorith}</code> such as created by {@link #crypt(String, String)
- * or if the extracted string isn't an available algorithm.
- */
- private static String extractAlgorithm(String hashedPwd) {
- int end = hashedPwd.indexOf('}');
- if (hashedPwd.startsWith("{") && end > 0) {
- String algorithm = hashedPwd.substring(1, end);
- try {
- MessageDigest.getInstance(algorithm);
- return algorithm;
- } catch (NoSuchAlgorithmException e) {
- log.debug("Invalid algorithm detected " + algorithm);
- }
- }
-
- // not starting with {} or invalid algorithm
- return null;
- }
-
- /**
- * Extract the salt from the password hash.
- *
- * @param hashedPwd
- * @param algorithm
- * @return salt or <code>null</code>
- */
- private static String extractSalt(String hashedPwd, String algorithm) {
- int start = algorithm.length()+2;
- int end = hashedPwd.indexOf('-', start);
- if (end > -1) {
- return hashedPwd.substring(start, end);
- }
-
- // no salt
- return null;
- }
-
- /**
- * Generate a new random salt for password digest.
- *
- * @return a new random salt.
- */
- private static String generateSalt() {
- SecureRandom random = new SecureRandom();
- byte salt[] = new byte[8];
- random.nextBytes(salt);
-
- StringBuffer res = new StringBuffer(salt.length * 2);
- for (byte b : salt) {
- res.append(Text.hexTable[(b >> 4) & 15]);
- res.append(Text.hexTable[b & 15]);
- }
- return res.toString();
- }
}
Added: jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/PasswordUtility.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/PasswordUtility.java?rev=1370420&view=auto
==============================================================================
--- jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/PasswordUtility.java (added)
+++ jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/PasswordUtility.java Tue Aug 7 18:21:11 2012
@@ -0,0 +1,239 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.jackrabbit.core.security.user;
+
+import org.apache.jackrabbit.util.Text;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.UnsupportedEncodingException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+
+/**
+ * Utility to generate and compare password hashes.
+ */
+public class PasswordUtility {
+
+ private static final Logger log = LoggerFactory.getLogger(PasswordUtility.class);
+
+ private static final char DELIMITER = '-';
+ private static final int NO_ITERATIONS = 1;
+ private static final String ENCODING = "UTF-8";
+
+ public static final String DEFAULT_ALGORITHM = "SHA-256";
+ public static final int DEFAULT_SALT_SIZE = 8;
+ public static final int DEFAULT_ITERATIONS = 1000;
+
+ /**
+ * Avoid instantiation
+ */
+ private PasswordUtility() {}
+
+ /**
+ * Generates a hash of the specified password with the default values
+ * for algorithm, salt-size and number of iterations.
+ *
+ * @param password The password to be hashed.
+ * @return The password hash.
+ * @throws NoSuchAlgorithmException If {@link #DEFAULT_ALGORITHM} is not supported.
+ * @throws UnsupportedEncodingException If utf-8 is not supported.
+ */
+ public static String buildPasswordHash(String password) throws NoSuchAlgorithmException, UnsupportedEncodingException {
+ return buildPasswordHash(password, DEFAULT_ALGORITHM, DEFAULT_SALT_SIZE, DEFAULT_ITERATIONS);
+ }
+
+ /**
+ * Generates a hash of the specified password using the specified algorithm,
+ * salt size and number of iterations into account.
+ *
+ * @param password The password to be hashed.
+ * @param algorithm The desired hash algorithm.
+ * @param saltSize The desired salt size. If the specified integer is lower
+ * that {@link #DEFAULT_SALT_SIZE} the default is used.
+ * @param iterations The desired number of iterations. If the specified
+ * integer is lower than 1 the {@link #DEFAULT_ITERATIONS default} value is used.
+ * @return The password hash.
+ * @throws NoSuchAlgorithmException If the specified algorithm is not supported.
+ * @throws UnsupportedEncodingException If utf-8 is not supported.
+ */
+ public static String buildPasswordHash(String password, String algorithm,
+ int saltSize, int iterations) throws NoSuchAlgorithmException, UnsupportedEncodingException {
+ if (password == null) {
+ throw new IllegalArgumentException("Password may not be null.");
+ }
+ if (iterations < NO_ITERATIONS) {
+ iterations = DEFAULT_ITERATIONS;
+ }
+ if (saltSize < DEFAULT_SALT_SIZE) {
+ saltSize = DEFAULT_SALT_SIZE;
+ }
+ String salt = generateSalt(saltSize);
+ String alg = (algorithm == null) ? DEFAULT_ALGORITHM : algorithm;
+ return generateHash(password, alg, salt, iterations);
+ }
+
+ /**
+ * Returns {@code true} if the specified string doesn't start with a
+ * valid algorithm name in curly brackets.
+ *
+ * @param password The string to be tested.
+ * @return {@code true} if the specified string doesn't start with a
+ * valid algorithm name in curly brackets.
+ */
+ public static boolean isPlainTextPassword(String password) {
+ return extractAlgorithm(password) == null;
+ }
+
+ /**
+ * Returns {@code true} if hash of the specified {@code password} equals the
+ * given hashed password.
+ *
+ * @param hashedPassword Password hash.
+ * @param password The password to compare.
+ * @return If the hash of the specified {@code password} equals the given
+ * {@code hashedPassword} string.
+ */
+ public static boolean isSame(String hashedPassword, String password) {
+ try {
+ String algorithm = extractAlgorithm(hashedPassword);
+ if (algorithm != null) {
+ int startPos = algorithm.length()+2;
+ String salt = extractSalt(hashedPassword, startPos);
+ int iterations = NO_ITERATIONS;
+ if (salt != null) {
+ startPos += salt.length()+1;
+ iterations = extractIterations(hashedPassword, startPos);
+ }
+
+ String hash = generateHash(password, algorithm, salt, iterations);
+ return hashedPassword.equals(hash);
+ } // hashedPassword is plaintext -> return false
+ } catch (NoSuchAlgorithmException e) {
+ log.warn(e.getMessage());
+ } catch (UnsupportedEncodingException e) {
+ log.warn(e.getMessage());
+ }
+ return false;
+ }
+
+ /**
+ * Extract the algorithm from the given crypted password string. Returns the
+ * algorithm or {@code null} if the given string doesn't have a
+ * leading {@code algorithm} such as created by {@code buildPasswordHash}
+ * or if the extracted string doesn't represent an available algorithm.
+ *
+ * @param hashedPwd The password hash.
+ * @return The algorithm or {@code null} if the given string doesn't have a
+ * leading {@code algorithm} such as created by {@code buildPasswordHash}
+ * or if the extracted string isn't a supported algorithm.
+ */
+ public static String extractAlgorithm(String hashedPwd) {
+ if (hashedPwd != null && hashedPwd.length() > 0) {
+ int end = hashedPwd.indexOf('}');
+ if (hashedPwd.charAt(0) == '{' && end > 0 && end < hashedPwd.length()-1) {
+ String algorithm = hashedPwd.substring(1, end);
+ try {
+ MessageDigest.getInstance(algorithm);
+ return algorithm;
+ } catch (NoSuchAlgorithmException e) {
+ log.debug("Invalid algorithm detected " + algorithm);
+ }
+ }
+ }
+
+ // not starting with {} or invalid algorithm
+ return null;
+ }
+
+ //------------------------------------------------------------< private >---
+
+ private static String generateHash(String pwd, String algorithm, String salt, int iterations) throws NoSuchAlgorithmException, UnsupportedEncodingException {
+ StringBuilder passwordHash = new StringBuilder();
+ passwordHash.append('{').append(algorithm).append('}');
+ if (salt != null && salt.length() > 0) {
+ StringBuilder data = new StringBuilder();
+ data.append(salt).append(pwd);
+
+ passwordHash.append(salt).append(DELIMITER);
+ if (iterations > NO_ITERATIONS) {
+ passwordHash.append(iterations).append(DELIMITER);
+ }
+ passwordHash.append(generateDigest(data.toString(), algorithm, iterations));
+ } else {
+ // backwards compatible to jr 2.0: no salt, no iterations
+ passwordHash.append(Text.digest(algorithm, pwd.getBytes(ENCODING)));
+ }
+ return passwordHash.toString();
+ }
+
+ private static String generateSalt(int saltSize) {
+ SecureRandom random = new SecureRandom();
+ byte[] salt = new byte[saltSize];
+ random.nextBytes(salt);
+
+ StringBuilder res = new StringBuilder(salt.length * 2);
+ for (byte b : salt) {
+ res.append(Text.hexTable[(b >> 4) & 15]);
+ res.append(Text.hexTable[b & 15]);
+ }
+ return res.toString();
+ }
+
+ private static String generateDigest(String data, String algorithm, int iterations) throws UnsupportedEncodingException, NoSuchAlgorithmException {
+ byte[] bytes = data.getBytes(ENCODING);
+ MessageDigest md = MessageDigest.getInstance(algorithm);
+
+ for (int i = 0; i < iterations; i++) {
+ md.reset();
+ bytes = md.digest(bytes);
+ }
+
+ StringBuilder res = new StringBuilder(bytes.length * 2);
+ for (byte b : bytes) {
+ res.append(Text.hexTable[(b >> 4) & 15]);
+ res.append(Text.hexTable[b & 15]);
+ }
+ return res.toString();
+ }
+
+ private static String extractSalt(String hashedPwd, int start) {
+ int end = hashedPwd.indexOf(DELIMITER, start);
+ if (end > -1) {
+ return hashedPwd.substring(start, end);
+ }
+ // no salt
+ return null;
+ }
+
+ private static int extractIterations(String hashedPwd, int start) {
+ int end = hashedPwd.indexOf(DELIMITER, start);
+ if (end > -1) {
+ String str = hashedPwd.substring(start, end);
+ try {
+ return Integer.parseInt(str);
+ } catch (NumberFormatException e) {
+ log.debug("Expected number of iterations. Found: " + str);
+ }
+ }
+
+ // no extra iterations
+ return NO_ITERATIONS;
+ }
+}
Modified: jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/UserImpl.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/UserImpl.java?rev=1370420&r1=1370419&r2=1370420&view=diff
==============================================================================
--- jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/UserImpl.java (original)
+++ jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/UserImpl.java Tue Aug 7 18:21:11 2012
@@ -26,7 +26,6 @@ import org.apache.jackrabbit.core.securi
import javax.jcr.Credentials;
import javax.jcr.RepositoryException;
-import javax.jcr.SimpleCredentials;
import javax.jcr.Value;
import java.io.UnsupportedEncodingException;
import java.security.NoSuchAlgorithmException;
@@ -44,19 +43,6 @@ public class UserImpl extends Authorizab
super(node, userManager);
}
- //--------------------------------------------------------------------------
- /**
- * Creates a hash of the specified password if it is found to be plain text.
- *
- * @param password The password string.
- * @return Hash for the given password string.
- * @throws RepositoryException If an error occurs.
- * @see CryptedSimpleCredentials#buildPasswordHash(String)
- */
- static String buildPasswordValue(String password) throws RepositoryException {
- return CryptedSimpleCredentials.buildPasswordHash(password);
- }
-
//-------------------------------------------------------< Authorizable >---
/**
* @see org.apache.jackrabbit.api.security.user.Authorizable#isGroup()
@@ -122,8 +108,10 @@ public class UserImpl extends Authorizab
*/
public void changePassword(String password) throws RepositoryException {
userManager.onPasswordChange(this, password);
- Value v = getSession().getValueFactory().createValue(buildPasswordValue(password));
- userManager.setProtectedProperty(getNode(), P_PASSWORD, v);
+ userManager.setPassword(getNode(), password, true);
+ if (userManager.isAutoSave()) {
+ getNode().save();
+ }
}
/**
@@ -131,18 +119,10 @@ public class UserImpl extends Authorizab
*/
public void changePassword(String password, String oldPassword) throws RepositoryException {
// make sure the old password matches.
- try {
- CryptedSimpleCredentials csc = (CryptedSimpleCredentials) getCredentials();
- SimpleCredentials creds = new SimpleCredentials(getID(), oldPassword.toCharArray());
- if (!csc.matches(creds)) {
- throw new RepositoryException("Failed to change password: Old password does not match.");
- }
- } catch (NoSuchAlgorithmException e) {
- throw new RepositoryException("Cannot change password: failed to validate old password.");
- } catch (UnsupportedEncodingException e) {
- throw new RepositoryException("Cannot change password: failed to validate old password.");
+ String pwHash = getNode().getProperty(P_PASSWORD).getString();
+ if (!PasswordUtility.isSame(pwHash, oldPassword)) {
+ throw new RepositoryException("Failed to change password: Old password does not match.");
}
-
changePassword(password);
}
Modified: jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/UserImporter.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/UserImporter.java?rev=1370420&r1=1370419&r2=1370420&view=diff
==============================================================================
--- jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/UserImporter.java (original)
+++ jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/UserImporter.java Tue Aug 7 18:21:11 2012
@@ -266,7 +266,7 @@ public class UserImporter implements Pro
Value v = protectedPropInfo.getValues(PropertyType.STRING, resolver)[0];
String pw = v.getString();
- ((User) a).changePassword(pw);
+ userManager.setPassword(parent, pw, false);
/*
Execute authorizable actions for a NEW user at this point after
Modified: jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/UserManagerImpl.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/UserManagerImpl.java?rev=1370420&r1=1370419&r2=1370420&view=diff
==============================================================================
--- jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/UserManagerImpl.java (original)
+++ jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/UserManagerImpl.java Tue Aug 7 18:21:11 2012
@@ -51,6 +51,7 @@ import javax.jcr.lock.LockException;
import javax.jcr.nodetype.ConstraintViolationException;
import javax.jcr.version.VersionException;
import java.io.UnsupportedEncodingException;
+import java.security.NoSuchAlgorithmException;
import java.security.Principal;
import java.util.HashSet;
import java.util.Iterator;
@@ -154,6 +155,12 @@ import java.util.UUID;
* instead of the default multi valued property {@link UserConstants#P_MEMBERS}.
* Its value determines the maximum number of member properties until additional
* intermediate nodes are inserted. Valid parameter values are integers > 4.</li>
+ * <li>{@link #PARAM_PASSWORD_HASH_ALGORITHM}: Optional parameter to configure
+ * the algorithm used for password hash generation. The default value is
+ * {@link PasswordUtility#DEFAULT_ALGORITHM}.</li>
+ * <li>{@link #PARAM_PASSWORD_HASH_ITERATIONS}: Optional parameter to configure
+ * the number of iterations used for password hash generations. The default
+ * value is {@link PasswordUtility#DEFAULT_ITERATIONS}.</li>
* </ul>
*
* <h4>Authorizable Actions</h4>
@@ -241,6 +248,18 @@ public class UserManagerImpl extends Pro
*/
public static final String PARAM_GROUP_MEMBERSHIP_SPLIT_SIZE = "groupMembershipSplitSize";
+ /**
+ * Configuration parameter to change the default algorithm used to generate
+ * password hashes. The default value is {@link PasswordUtility#DEFAULT_ALGORITHM}.
+ */
+ public static final String PARAM_PASSWORD_HASH_ALGORITHM = "passwordHashAlgorithm";
+
+ /**
+ * Configuration parameter to change the number of iterations used for
+ * password hash generation. The default value is {@link PasswordUtility#DEFAULT_ITERATIONS}.
+ */
+ public static final String PARAM_PASSWORD_HASH_ITERATIONS = "passwordHashIterations";
+
private static final Logger log = LoggerFactory.getLogger(UserManagerImpl.class);
private final SessionImpl session;
@@ -550,15 +569,14 @@ public class UserManagerImpl extends Pro
Principal principal, String intermediatePath)
throws AuthorizableExistsException, RepositoryException {
checkValidID(userID);
- if (password == null) {
- throw new IllegalArgumentException("Cannot create user: null password.");
- }
+
+ // NOTE: password validation during setPassword and onCreate.
// NOTE: principal validation during setPrincipal call.
try {
NodeImpl userNode = (NodeImpl) nodeCreator.createUserNode(userID, intermediatePath);
setPrincipal(userNode, principal);
- setProperty(userNode, P_PASSWORD, getValue(UserImpl.buildPasswordValue(password)), true);
+ setPassword(userNode, password, true);
User user = createUser(userNode);
onCreate(user, password);
@@ -706,6 +724,39 @@ public class UserManagerImpl extends Pro
setProperty(node, P_PRINCIPAL_NAME, getValue(principal.getName()), true);
}
+ /**
+ * Generate a password value from the specified string and set the
+ * {@link UserConstants#P_PASSWORD} property to the given user node.
+ *
+ * @param userNode A user node.
+ * @param password The password value.
+ * @param forceHash If <code>true</code> the specified password string will
+ * always be hashed; otherwise the hash will only be generated if it appears
+ * to be a {@link PasswordUtility#isPlainTextPassword(String) plain text} password.
+ * @throws RepositoryException If an exception occurs.
+ */
+ void setPassword(NodeImpl userNode, String password, boolean forceHash) throws RepositoryException {
+ if (password == null) {
+ throw new IllegalArgumentException("Password may not be null.");
+ }
+ String pwHash;
+ if (forceHash || PasswordUtility.isPlainTextPassword(password)) {
+ try {
+ String algorithm = config.getConfigValue(PARAM_PASSWORD_HASH_ALGORITHM, PasswordUtility.DEFAULT_ALGORITHM);
+ int iterations = config.getConfigValue(PARAM_PASSWORD_HASH_ITERATIONS, PasswordUtility.DEFAULT_ITERATIONS);
+ pwHash = PasswordUtility.buildPasswordHash(password, algorithm, PasswordUtility.DEFAULT_SALT_SIZE, iterations);
+ } catch (NoSuchAlgorithmException e) {
+ throw new RepositoryException(e);
+ } catch (UnsupportedEncodingException e) {
+ throw new RepositoryException(e);
+ }
+ } else {
+ pwHash = password;
+ }
+ Value v = getSession().getValueFactory().createValue(pwHash);
+ setProperty(userNode, P_PASSWORD, getValue(pwHash), userNode.isNew());
+ }
+
void setProtectedProperty(NodeImpl node, Name propName, Value value) throws RepositoryException, LockException, ConstraintViolationException, ItemExistsException, VersionException {
setProperty(node, propName, value);
if (isAutoSave()) {
Modified: jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/action/PasswordValidationAction.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/action/PasswordValidationAction.java?rev=1370420&r1=1370419&r2=1370420&view=diff
==============================================================================
--- jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/action/PasswordValidationAction.java (original)
+++ jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/action/PasswordValidationAction.java Tue Aug 7 18:21:11 2012
@@ -17,7 +17,7 @@
package org.apache.jackrabbit.core.security.user.action;
import org.apache.jackrabbit.api.security.user.User;
-import org.apache.jackrabbit.core.security.authentication.CryptedSimpleCredentials;
+import org.apache.jackrabbit.core.security.user.PasswordUtility;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -70,12 +70,12 @@ public class PasswordValidationAction ex
//-------------------------------------------------< AuthorizableAction >---
@Override
public void onCreate(User user, String password, Session session) throws RepositoryException {
- validatePassword(password);
+ validatePassword(password, false); // don't force validation of hashed passwords.
}
@Override
public void onPasswordChange(User user, String newPassword, Session session) throws RepositoryException {
- validatePassword(newPassword);
+ validatePassword(newPassword, true); // force validation of all passwords
}
//---------------------------------------------------------< BeanConfig >---
@@ -97,23 +97,16 @@ public class PasswordValidationAction ex
* Validate the specified password.
*
* @param password The password to be validated
+ * @param forceMatch If true the specified password is always validated;
+ * otherwise only if it is a plain text password.
* @throws RepositoryException If the specified password is too short or
* doesn't match the specified password pattern.
*/
- private void validatePassword(String password) throws RepositoryException {
- if (password != null && isPlainText(password)) {
+ private void validatePassword(String password, boolean forceMatch) throws RepositoryException {
+ if (password != null && (forceMatch || PasswordUtility.isPlainTextPassword(password))) {
if (pattern != null && !pattern.matcher(password).matches()) {
throw new ConstraintViolationException("Password violates password constraint (" + pattern.pattern() + ").");
}
}
}
-
- private static boolean isPlainText(String password) {
- try {
- return !CryptedSimpleCredentials.buildPasswordHash(password).equals(password);
- } catch (RepositoryException e) {
- // failed to build hash from pw -> proceed with the validation.
- return true;
- }
- }
}
\ No newline at end of file
Modified: jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/AuthorizableActionTest.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/AuthorizableActionTest.java?rev=1370420&r1=1370419&r2=1370420&view=diff
==============================================================================
--- jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/AuthorizableActionTest.java (original)
+++ jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/AuthorizableActionTest.java Tue Aug 7 18:21:11 2012
@@ -230,7 +230,27 @@ public class AuthorizableActionTest exte
}
}
- public void testPasswordValidationActionIgnoresHashedPwString() throws Exception {
+ public void testPasswordValidationActionIgnoresHashedPwStringOnCreate() throws Exception {
+ User u = null;
+
+ try {
+ PasswordValidationAction pwAction = new PasswordValidationAction();
+ pwAction.setConstraint("^.*(?=.{8,})(?=.*[a-z])(?=.*[A-Z]).*");
+ setActions(pwAction);
+
+ String uid = getTestPrincipal().getName();
+ String hashed = PasswordUtility.buildPasswordHash("DWkej32H");
+ u = impl.createUser(uid, hashed);
+
+ } finally {
+ if (u != null) {
+ u.remove();
+ }
+ save(superuser);
+ }
+ }
+
+ public void testPasswordValidationActionOnChange() throws Exception {
User u = null;
try {
@@ -238,12 +258,16 @@ public class AuthorizableActionTest exte
u = impl.createUser(uid, buildPassword(uid));
PasswordValidationAction pwAction = new PasswordValidationAction();
- pwAction.setConstraint("^.*(?=.{8,})(?=.*[a-z])(?=.*[A-Z]).*");
+ pwAction.setConstraint("abc");
setActions(pwAction);
- String hashed = ((UserImpl) u).buildPasswordValue("DWkej32H");
+ String hashed = PasswordUtility.buildPasswordHash("abc");
u.changePassword(hashed);
+ fail("Password change must always enforce password validation.");
+
+ } catch (ConstraintViolationException e) {
+ // success
} finally {
if (u != null) {
u.remove();
Added: jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/PasswordUtilityTest.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/PasswordUtilityTest.java?rev=1370420&view=auto
==============================================================================
--- jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/PasswordUtilityTest.java (added)
+++ jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/PasswordUtilityTest.java Tue Aug 7 18:21:11 2012
@@ -0,0 +1,170 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.jackrabbit.core.security.user;
+
+import org.apache.jackrabbit.core.security.SecurityConstants;
+import org.apache.jackrabbit.test.JUnitTest;
+
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * <code>PasswordUtilityTest</code>...
+ */
+public class PasswordUtilityTest extends JUnitTest {
+
+ private static List<String> PLAIN_PWDS = new ArrayList<String>();
+ static {
+ PLAIN_PWDS.add("pw");
+ PLAIN_PWDS.add("PassWord123");
+ PLAIN_PWDS.add("_");
+ PLAIN_PWDS.add("{invalidAlgo}");
+ PLAIN_PWDS.add("{invalidAlgo}Password");
+ PLAIN_PWDS.add("{SHA-256}");
+ PLAIN_PWDS.add("pw{SHA-256}");
+ PLAIN_PWDS.add("p{SHA-256}w");
+ PLAIN_PWDS.add("");
+ }
+
+ private static Map<String, String> HASHED_PWDS = new HashMap<String, String>();
+ static {
+ for (String pw : PLAIN_PWDS) {
+ try {
+ HASHED_PWDS.put(pw, PasswordUtility.buildPasswordHash(pw));
+ } catch (Exception e) {
+ // should not get here
+ }
+ }
+ }
+
+ public void testBuildPasswordHash() throws Exception {
+ for (String pw : PLAIN_PWDS) {
+ String pwHash = PasswordUtility.buildPasswordHash(pw);
+ assertFalse(pw.equals(pwHash));
+ }
+
+ List<Integer[]> l = new ArrayList<Integer[]>();
+ l.add(new Integer[] {0, 1000});
+ l.add(new Integer[] {1, 10});
+ l.add(new Integer[] {8, 50});
+ l.add(new Integer[] {10, 5});
+ l.add(new Integer[] {-1, -1});
+ for (Integer[] params : l) {
+ for (String pw : PLAIN_PWDS) {
+ int saltsize = params[0];
+ int iterations = params[1];
+
+ String pwHash = PasswordUtility.buildPasswordHash(pw, PasswordUtility.DEFAULT_ALGORITHM, saltsize, iterations);
+ assertFalse(pw.equals(pwHash));
+ }
+ }
+ }
+
+ public void testBuildPasswordHashInvalidAlgorithm() throws Exception {
+ List<String> invalidAlgorithms = new ArrayList<String>();
+ invalidAlgorithms.add("");
+ invalidAlgorithms.add("+");
+ invalidAlgorithms.add("invalid");
+
+ for (String invalid : invalidAlgorithms) {
+ try {
+ String pwHash = PasswordUtility.buildPasswordHash("pw", invalid, PasswordUtility.DEFAULT_SALT_SIZE, PasswordUtility.DEFAULT_ITERATIONS);
+ fail("Invalid algorithm " + invalid);
+ } catch (NoSuchAlgorithmException e) {
+ // success
+ }
+ }
+
+ }
+
+ public void testIsPlainTextPassword() throws Exception {
+ for (String pw : PLAIN_PWDS) {
+ assertTrue(pw + " should be plain text.", PasswordUtility.isPlainTextPassword(pw));
+ }
+ }
+
+ public void testIsPlainTextForNull() throws Exception {
+ assertTrue(PasswordUtility.isPlainTextPassword(null));
+ }
+
+ public void testIsPlainTextForPwHash() throws Exception {
+ for (String pwHash : HASHED_PWDS.values()) {
+ assertFalse(pwHash + " should not be plain text.", PasswordUtility.isPlainTextPassword(pwHash));
+ }
+ }
+
+ public void testIsSame() throws Exception {
+ for (String pw : HASHED_PWDS.keySet()) {
+ String pwHash = HASHED_PWDS.get(pw);
+ assertTrue("Not the same " + pw + ", " + pwHash, PasswordUtility.isSame(pwHash, pw));
+ }
+
+ String pw = "password";
+ String pwHash = PasswordUtility.buildPasswordHash(pw, SecurityConstants.DEFAULT_DIGEST, 4, 50);
+ assertTrue("Not the same '" + pw + "', " + pwHash, PasswordUtility.isSame(pwHash, pw));
+
+ pwHash = PasswordUtility.buildPasswordHash(pw, "md5", 0, 5);
+ assertTrue("Not the same '" + pw + "', " + pwHash, PasswordUtility.isSame(pwHash, pw));
+
+ pwHash = PasswordUtility.buildPasswordHash(pw, "md5", -1, -1);
+ assertTrue("Not the same '" + pw + "', " + pwHash, PasswordUtility.isSame(pwHash, pw));
+ }
+
+ public void testIsNotSame() throws Exception {
+ String previous = null;
+ for (String pw : HASHED_PWDS.keySet()) {
+ String pwHash = HASHED_PWDS.get(pw);
+ assertFalse(pw, PasswordUtility.isSame(pw, pw));
+ assertFalse(pwHash, PasswordUtility.isSame(pwHash, pwHash));
+ if (previous != null) {
+ assertFalse(previous, PasswordUtility.isSame(pwHash, previous));
+ }
+ previous = pw;
+ }
+ }
+
+ public void testExtractAlgorithmFromPlainPw() throws Exception {
+ for (String pw : PLAIN_PWDS) {
+ assertNull(pw + " is no pw-hash -> no algorithm expected.", PasswordUtility.extractAlgorithm(pw));
+ }
+ }
+
+ public void testExtractAlgorithmFromNull() throws Exception {
+ assertNull("null pw -> no algorithm expected.", PasswordUtility.extractAlgorithm(null));
+ }
+
+ public void testExtractAlgorithmFromPwHash() throws Exception {
+ for (String pwHash : HASHED_PWDS.values()) {
+ String algorithm = PasswordUtility.extractAlgorithm(pwHash);
+ assertNotNull(pwHash + " is pw-hash -> algorithm expected.", algorithm);
+ assertEquals("Wrong algorithm extracted from " + pwHash, PasswordUtility.DEFAULT_ALGORITHM, algorithm);
+ }
+
+ String pwHash = PasswordUtility.buildPasswordHash("pw", SecurityConstants.DEFAULT_DIGEST, 4, 50);
+ assertEquals(SecurityConstants.DEFAULT_DIGEST, PasswordUtility.extractAlgorithm(pwHash));
+
+ pwHash = PasswordUtility.buildPasswordHash("pw", "md5", 0, 5);
+ assertEquals("md5", PasswordUtility.extractAlgorithm(pwHash));
+
+ pwHash = PasswordUtility.buildPasswordHash("pw", "md5", -1, -1);
+ assertEquals("md5", PasswordUtility.extractAlgorithm(pwHash));
+ }
+}
Modified: jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/TestAll.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/TestAll.java?rev=1370420&r1=1370419&r2=1370420&view=diff
==============================================================================
--- jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/TestAll.java (original)
+++ jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/TestAll.java Tue Aug 7 18:21:11 2012
@@ -54,6 +54,7 @@ public class TestAll extends TestCase {
suite.addTestSuite(UserAccessControlProviderTest.class);
suite.addTestSuite(DefaultPrincipalProviderTest.class);
+ suite.addTestSuite(PasswordUtilityTest.class);
return suite;
}
}
Modified: jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/UserImplTest.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/UserImplTest.java?rev=1370420&r1=1370419&r2=1370420&view=diff
==============================================================================
--- jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/UserImplTest.java (original)
+++ jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/UserImplTest.java Tue Aug 7 18:21:11 2012
@@ -142,9 +142,11 @@ public class UserImplTest extends Abstra
// plain text passwords
pwds.put("abc", "abc");
pwds.put("{a}password", "{a}password");
- // passwords already in hashed format.
- pwds.put(sha1Hash, "abc");
- pwds.put(md5Hash, "abc");
+ // passwords with hash-like char-sequence -> must still be hashed.
+ pwds.put(sha1Hash, sha1Hash);
+ pwds.put(md5Hash, md5Hash);
+ pwds.put("{"+SecurityConstants.DEFAULT_DIGEST+"}any", "{"+SecurityConstants.DEFAULT_DIGEST+"}any");
+ pwds.put("{"+SecurityConstants.DEFAULT_DIGEST+"}", "{"+SecurityConstants.DEFAULT_DIGEST+"}");
for (String pw : pwds.keySet()) {
u.changePassword(pw);
@@ -159,11 +161,9 @@ public class UserImplTest extends Abstra
// valid passwords, non-matching plain text
Map<String, String>noMatch = new HashMap<String, String>();
noMatch.put("{"+SecurityConstants.DEFAULT_DIGEST+"}", "");
- noMatch.put("{"+SecurityConstants.DEFAULT_DIGEST+"}", "{"+SecurityConstants.DEFAULT_DIGEST+"}");
noMatch.put("{"+SecurityConstants.DEFAULT_DIGEST+"}any", "any");
- noMatch.put("{"+SecurityConstants.DEFAULT_DIGEST+"}any", "{"+SecurityConstants.DEFAULT_DIGEST+"}any");
- noMatch.put(sha1Hash, sha1Hash);
- noMatch.put(md5Hash, md5Hash);
+ noMatch.put(sha1Hash, "abc");
+ noMatch.put(md5Hash, "abc");
for (String pw : noMatch.keySet()) {
u.changePassword(pw);
@@ -172,10 +172,14 @@ public class UserImplTest extends Abstra
SimpleCredentials sc = new SimpleCredentials(u.getID(), plain.toCharArray());
CryptedSimpleCredentials cc = (CryptedSimpleCredentials) u.getCredentials();
- assertFalse(cc.matches(sc));
+ assertFalse(pw, cc.matches(sc));
}
+ }
+
+ public void testChangePasswordNull() throws RepositoryException {
+ User u = (User) userMgr.getAuthorizable(uID);
- // invalid pw string
+ // invalid 'null' pw string
try {
u.changePassword(null);
fail("invalid pw null");