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 &gt; 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");