shiro-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From lhazlew...@apache.org
Subject svn commit: r1213449 - in /shiro/trunk/core/src: main/java/org/apache/shiro/authc/credential/ main/java/org/apache/shiro/crypto/hash/ main/java/org/apache/shiro/crypto/hash/format/ test/groovy/org/apache/shiro/authc/credential/ test/groovy/org/apache/s...
Date Mon, 12 Dec 2011 21:41:44 GMT
Author: lhazlewood
Date: Mon Dec 12 21:41:43 2011
New Revision: 1213449

URL: http://svn.apache.org/viewvc?rev=1213449&view=rev
Log:
SHIRO-280: added unit tests for DefaultPasswordService, PasswordMatcher, and HashRequest.Builder

Added:
    shiro/trunk/core/src/main/java/org/apache/shiro/authc/credential/HashingPasswordService.java
    shiro/trunk/core/src/test/groovy/org/apache/shiro/authc/credential/PasswordMatcherTest.groovy
    shiro/trunk/core/src/test/groovy/org/apache/shiro/crypto/hash/DefaultHashServiceTest.groovy
    shiro/trunk/core/src/test/groovy/org/apache/shiro/crypto/hash/HashRequestBuilderTest.groovy
Removed:
    shiro/trunk/core/src/test/java/org/apache/shiro/crypto/hash/DefaultHashServiceTest.java
Modified:
    shiro/trunk/core/src/main/java/org/apache/shiro/authc/credential/DefaultPasswordService.java
    shiro/trunk/core/src/main/java/org/apache/shiro/authc/credential/PasswordMatcher.java
    shiro/trunk/core/src/main/java/org/apache/shiro/authc/credential/PasswordService.java
    shiro/trunk/core/src/main/java/org/apache/shiro/crypto/hash/DefaultHashService.java
    shiro/trunk/core/src/main/java/org/apache/shiro/crypto/hash/format/HashFormatFactory.java
    shiro/trunk/core/src/test/groovy/org/apache/shiro/authc/credential/DefaultPasswordServiceTest.groovy

Modified: shiro/trunk/core/src/main/java/org/apache/shiro/authc/credential/DefaultPasswordService.java
URL: http://svn.apache.org/viewvc/shiro/trunk/core/src/main/java/org/apache/shiro/authc/credential/DefaultPasswordService.java?rev=1213449&r1=1213448&r2=1213449&view=diff
==============================================================================
--- shiro/trunk/core/src/main/java/org/apache/shiro/authc/credential/DefaultPasswordService.java (original)
+++ shiro/trunk/core/src/main/java/org/apache/shiro/authc/credential/DefaultPasswordService.java Mon Dec 12 21:41:43 2011
@@ -18,156 +18,183 @@
  */
 package org.apache.shiro.authc.credential;
 
-import org.apache.shiro.codec.Base64;
-import org.apache.shiro.crypto.RandomNumberGenerator;
-import org.apache.shiro.crypto.SecureRandomNumberGenerator;
+import org.apache.shiro.crypto.hash.DefaultHashService;
 import org.apache.shiro.crypto.hash.Hash;
-import org.apache.shiro.crypto.hash.SimpleHash;
+import org.apache.shiro.crypto.hash.HashRequest;
+import org.apache.shiro.crypto.hash.HashService;
+import org.apache.shiro.crypto.hash.format.*;
 import org.apache.shiro.util.ByteSource;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.util.Arrays;
-
 /**
- * Default implementation of the {@link PasswordService} interface.
+ * Default implementation of the {@link PasswordService} interface that relies on an internal
+ * {@link HashService}, {@link HashFormat}, and {@link HashFormatFactory} to function:
+ * <h2>Hashing Passwords</h2>
+ *
+ * <h2>Comparing Passwords</h2>
+ * All hashing operations are performed by the internal {@link #getHashService() hashService}.  After the hash
+ * is computed, it is formatted into a String value via the internal {@link #getHashFormat() hashFormat}.
  *
  * @since 1.2
  */
-public class DefaultPasswordService implements PasswordService {
+public class DefaultPasswordService implements HashingPasswordService {
 
-    public static final String DEFAULT_HASH_ALGORITHM_NAME = "SHA-512";
-    //see http://www.katasoft.com/blog/2011/04/04/strong-password-hashing-apache-shiro
+    public static final String DEFAULT_HASH_ALGORITHM = "SHA-256";
     public static final int DEFAULT_HASH_ITERATIONS = 500000; //500,000
-    public static final int DEFAULT_SALT_SIZE = 32; //32 bytes == 256 bits
-
-    private static final String MCF_PREFIX = "$shiro1$"; //Modular Crypt Format prefix specific to Shiro's needs
 
     private static final Logger log = LoggerFactory.getLogger(DefaultPasswordService.class);
 
-    private String hashAlgorithmName;
-    private int hashIterations;
-    private int saltSize;
-    private RandomNumberGenerator randomNumberGenerator;
+    private HashService hashService;
+    private HashFormat hashFormat;
+    private HashFormatFactory hashFormatFactory;
+
+    private volatile boolean hashFormatWarned; //used to avoid excessive log noise
 
     public DefaultPasswordService() {
-        this.hashAlgorithmName = DEFAULT_HASH_ALGORITHM_NAME;
-        this.hashIterations = DEFAULT_HASH_ITERATIONS;
-        this.saltSize = DEFAULT_SALT_SIZE;
-        this.randomNumberGenerator = new SecureRandomNumberGenerator();
+        this.hashFormatWarned = false;
+
+        DefaultHashService hashService = new DefaultHashService();
+        hashService.setHashAlgorithmName(DEFAULT_HASH_ALGORITHM);
+        hashService.setHashIterations(DEFAULT_HASH_ITERATIONS);
+        hashService.setGeneratePublicSalt(true); //always want generated salts for user passwords to be most secure
+        this.hashService = hashService;
+
+        this.hashFormat = new Shiro1CryptFormat();
+        this.hashFormatFactory = new DefaultHashFormatFactory();
     }
 
-    public String hashPassword(String plaintext) {
-        if (plaintext == null || plaintext.length() == 0) {
-            return null;
-        }
-        return hashPassword(ByteSource.Util.bytes(plaintext));
+    public String encryptPassword(Object plaintext) {
+        Hash hash = hashPassword(plaintext);
+        checkHashFormatDurability();
+        return this.hashFormat.format(hash);
     }
 
-    public String hashPassword(ByteSource plaintext) {
-        if (plaintext == null) {
+    public Hash hashPassword(Object plaintext) {
+        ByteSource plaintextBytes = createByteSource(plaintext);
+        if (plaintextBytes == null || plaintextBytes.isEmpty()) {
             return null;
         }
-        byte[] plaintextBytes = plaintext.getBytes();
-        if (plaintextBytes == null || plaintextBytes.length == 0) {
-            return null;
+        HashRequest request = createHashRequest(plaintextBytes);
+        return hashService.computeHash(request);
+    }
+
+    public boolean passwordsMatch(Object plaintext, Hash saved) {
+        ByteSource plaintextBytes = createByteSource(plaintext);
+
+        if (saved == null || saved.isEmpty()) {
+            return plaintextBytes == null || plaintextBytes.isEmpty();
+        } else {
+            if (plaintextBytes == null || plaintextBytes.isEmpty()) {
+                return false;
+            }
         }
-        String algorithmName = getHashAlgorithmName();
-        ByteSource salt = getRandomNumberGenerator().nextBytes(getSaltSize());
-        int iterations = Math.max(1, getHashIterations());
 
-        Hash result = new SimpleHash(algorithmName, plaintext, salt, iterations);
+        HashRequest request = buildHashRequest(plaintextBytes, saved);
 
-        //Modular Crypt Format
-        //TODO: make this pluggable:
-        return new StringBuilder(MCF_PREFIX).append(algorithmName).append("$").append(iterations).append("$")
-                .append(salt.toBase64()).append("$").append(result.toBase64()).toString();
+        Hash computed = this.hashService.computeHash(request);
+
+        return saved.equals(computed);
     }
 
-    public boolean passwordsMatch(ByteSource submittedPassword, String savedPassword) {
-        if (savedPassword == null) {
-            return isEmpty(submittedPassword);
-        } else {
-            return !isEmpty(submittedPassword) && doPasswordsMatch(submittedPassword, savedPassword);
+    protected void checkHashFormatDurability() {
+
+        if (!this.hashFormatWarned) {
+
+            HashFormat format = this.hashFormat;
+
+            if (!(format instanceof ParsableHashFormat) && log.isWarnEnabled()) {
+                String msg = "The configured hashFormat instance [" + format.getClass().getName() + "] is not a " +
+                        ParsableHashFormat.class.getName() + " implementation.  This is " +
+                        "required if you wish to support backwards compatibility for saved password checking (almost " +
+                        "always desirable).  Without a " + ParsableHashFormat.class.getSimpleName() + " instance, " +
+                        "any hashService configuration changes will break previously hashed/saved passwords.";
+                log.warn(msg);
+                this.hashFormatWarned = true;
+            }
         }
     }
 
-    private static boolean isEmpty(ByteSource source) {
-        return source == null || source.getBytes() == null || source.getBytes().length == 0;
+    protected HashRequest createHashRequest(ByteSource plaintext) {
+        return new HashRequest.Builder().setSource(plaintext).build();
     }
 
-    private boolean doPasswordsMatch(ByteSource submittedPassword, String savedPassword) {
-        if (!savedPassword.startsWith(MCF_PREFIX)) {
-            log.warn("Encountered unrecognized saved password format.  Falling back to simple equality " +
-                    "comparison.  Use the PasswordService to hash new passwords as well as match them.");
-            return ByteSource.Util.bytes(savedPassword).equals(submittedPassword);
-        }
+    protected ByteSource createByteSource(Object o) {
+        return ByteSource.Util.bytes(o);
+    }
 
-        String suffix = savedPassword.substring(MCF_PREFIX.length());
-        String[] parts = suffix.split("\\$");
+    public boolean passwordsMatch(Object submittedPlaintext, String saved) {
+        ByteSource plaintextBytes = createByteSource(submittedPlaintext);
 
-        //last part is always the digest/checksum, Base64-encoded:
-        int i = parts.length-1;
-        String digestBase64 = parts[i--];
-        //second-to-last part is always the salt, Base64-encoded:
-        String saltBase64 = parts[i--];
-        String iterationsString = parts[i--];
-        String algorithmName = parts[i--];
-
-        /*String timestampString = null;
-
-        if (parts.length == 5) {
-            timestampString = parts[i--];
-        } */
-
-        byte[] digest = Base64.decode(digestBase64);
-
-        byte[] salt = Base64.decode(saltBase64);
-        int iterations;
-        try {
-            iterations = Integer.parseInt(iterationsString);
-        } catch (NumberFormatException e) {
-            log.error("Unable to parse saved password string: " + savedPassword, e);
-            throw e;
+        if (saved == null || saved.length() == 0) {
+            return plaintextBytes == null || plaintextBytes.isEmpty();
+        } else {
+            if (plaintextBytes == null || plaintextBytes.isEmpty()) {
+                return false;
+            }
         }
 
-        //now compute the digest on the submitted password.  If the resulting digest matches the saved digest,
-        //the password matches:
+        //First check to see if we can reconstitute the original hash - this allows us to
+        //perform password hash comparisons even for previously saved passwords that don't
+        //match the current HashService configuration values.  This is a very nice feature
+        //for password comparisons because it ensures backwards compatibility even after
+        //configuration changes.
+        HashFormat discoveredFormat = this.hashFormatFactory.getInstance(saved);
 
-        Hash submittedHash = new SimpleHash(algorithmName, submittedPassword, salt, iterations);
+        if (discoveredFormat != null && discoveredFormat instanceof ParsableHashFormat) {
 
-        return Arrays.equals(digest, submittedHash.getBytes());
-    }
+            ParsableHashFormat parsableHashFormat = (ParsableHashFormat)discoveredFormat;
+            Hash savedHash = parsableHashFormat.parse(saved);
+
+            return passwordsMatch(submittedPlaintext, savedHash);
+        }
+
+        //If we're at this point in the method's execution, We couldn't reconstitute the original hash.
+        //So, we need to hash the submittedPlaintext using current HashService configuration and then
+        //compare the formatted output with the saved string.  This will correctly compare passwords,
+        //but does not allow changing the HashService configuration without breaking previously saved
+        //passwords:
+
+        //The saved text value can't be reconstituted into a Hash instance.  We need to format the
+        //submittedPlaintext and then compare this formatted value with the saved value:
+        HashRequest request = createHashRequest(plaintextBytes);
+        Hash computed = this.hashService.computeHash(request);
+        String formatted = this.hashFormat.format(computed);
 
-    public String getHashAlgorithmName() {
-        return hashAlgorithmName;
+        return saved.equals(formatted);
     }
 
-    public void setHashAlgorithmName(String hashAlgorithmName) {
-        this.hashAlgorithmName = hashAlgorithmName;
+    protected HashRequest buildHashRequest(ByteSource plaintext, Hash saved) {
+        //keep everything from the saved hash except for the source:
+        return new HashRequest.Builder().setSource(plaintext)
+                //now use the existing saved data:
+                .setAlgorithmName(saved.getAlgorithmName())
+                .setSalt(saved.getSalt())
+                .setIterations(saved.getIterations())
+                .build();
     }
 
-    public int getHashIterations() {
-        return hashIterations;
+    public HashService getHashService() {
+        return hashService;
     }
 
-    public void setHashIterations(int hashIterations) {
-        this.hashIterations = hashIterations;
+    public void setHashService(HashService hashService) {
+        this.hashService = hashService;
     }
 
-    public int getSaltSize() {
-        return saltSize;
+    public HashFormat getHashFormat() {
+        return hashFormat;
     }
 
-    public void setSaltSize(int saltSize) {
-        this.saltSize = saltSize;
+    public void setHashFormat(HashFormat hashFormat) {
+        this.hashFormat = hashFormat;
     }
 
-    public RandomNumberGenerator getRandomNumberGenerator() {
-        return randomNumberGenerator;
+    public HashFormatFactory getHashFormatFactory() {
+        return hashFormatFactory;
     }
 
-    public void setRandomNumberGenerator(RandomNumberGenerator randomNumberGenerator) {
-        this.randomNumberGenerator = randomNumberGenerator;
+    public void setHashFormatFactory(HashFormatFactory hashFormatFactory) {
+        this.hashFormatFactory = hashFormatFactory;
     }
 }

Added: shiro/trunk/core/src/main/java/org/apache/shiro/authc/credential/HashingPasswordService.java
URL: http://svn.apache.org/viewvc/shiro/trunk/core/src/main/java/org/apache/shiro/authc/credential/HashingPasswordService.java?rev=1213449&view=auto
==============================================================================
--- shiro/trunk/core/src/main/java/org/apache/shiro/authc/credential/HashingPasswordService.java (added)
+++ shiro/trunk/core/src/main/java/org/apache/shiro/authc/credential/HashingPasswordService.java Mon Dec 12 21:41:43 2011
@@ -0,0 +1,91 @@
+/*
+ * 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.shiro.authc.credential;
+
+import org.apache.shiro.crypto.hash.Hash;
+import org.apache.shiro.util.ByteSource;
+
+/**
+ * A {@code HashingPasswordService} is a {@link PasswordService} that performs password encryption and comparisons
+ * based on cryptographic {@link Hash}es.
+ *
+ * @since 1.2
+ */
+public interface HashingPasswordService extends PasswordService {
+
+    /**
+     * Hashes the specified plaintext password using internal hashing configuration settings pertinent to password
+     * hashing.
+     * <p/>
+     * Note
+     * that this method is only likely to be used in more complex environments that wish to format and/or save the
+     * returned {@code Hash} object in a custom manner.  Most applications will find the
+     * {@link #encryptPassword(Object) encryptPassword} method suitable enough for safety
+     * and ease-of-use.
+     * <h3>Usage</h3>
+     * The input argument type can be any 'byte backed' {@code Object} - almost always either a
+     * String or character array representing passwords (character arrays are often a safer way to represent passwords
+     * as they can be cleared/nulled-out after use.  Any argument type supported by
+     * {@link ByteSource.Util#isCompatible(Object)} is valid.
+     * <p/>
+     * Regardless of your choice of using Strings or character arrays to represent submitted passwords, you can wrap
+     * either as a {@code ByteSource} by using {@link ByteSource.Util}, for example, when the passwords are captured as
+     * Strings:
+     * <pre>
+     * ByteSource passwordBytes = ByteSource.Util.bytes(submittedPasswordString);
+     * Hash hashedPassword = hashingPasswordService.hashPassword(passwordBytes);
+     * </pre>
+     * or, identically, when captured as a character array:
+     * <pre>
+     * ByteSource passwordBytes = ByteSource.Util.bytes(submittedPasswordCharacterArray);
+     * Hash hashedPassword = hashingPasswordService.hashPassword(passwordBytes);
+     * </pre>
+     *
+     * @param plaintext the raw password as 'byte-backed' object (String, character array, {@link ByteSource},
+     *                  etc) usually acquired from your application's 'new user' or 'password reset' workflow.
+     * @return the hashed password.
+     * @throws IllegalArgumentException if the argument cannot be easily converted to bytes as defined by
+     *                                  {@link ByteSource.Util#isCompatible(Object)}.
+     * @see ByteSource.Util#isCompatible(Object)
+     * @see #encryptPassword(Object)
+     */
+    Hash hashPassword(Object plaintext) throws IllegalArgumentException;
+
+    /**
+     * Returns {@code true} if the {@code submittedPlaintext} password matches the existing {@code savedPasswordHash},
+     * {@code false} otherwise.  Note that this method is only likely to be used in more complex environments that
+     * save hashes in a custom manner.  Most applications will find the
+     * {@link #passwordsMatch(Object, String) passwordsMatch(plaintext,string)} method
+     * sufficient if {@link #encryptPassword(Object) encrypting passwords as Strings}.
+     * <h3>Usage</h3>
+     * The {@code submittedPlaintext} argument type can be any 'byte backed' {@code Object} - almost always either a
+     * String or character array representing passwords (character arrays are often a safer way to represent passwords
+     * as they can be cleared/nulled-out after use.  Any argument type supported by
+     * {@link ByteSource.Util#isCompatible(Object)} is valid.
+     *
+     * @param plaintext a raw/plaintext password submitted by an end user/Subject.
+     * @param savedPasswordHash  the previously hashed password known to be associated with an account.
+     *                           This value is expected to have been previously generated from the
+     *                           {@link #hashPassword(Object) hashPassword} method (typically
+     *                           when the account is created or the account's password is reset).
+     * @return {@code true} if the {@code plaintext} password matches the existing {@code savedPasswordHash},
+     *         {@code false} otherwise.
+     */
+    boolean passwordsMatch(Object plaintext, Hash savedPasswordHash);
+}

Modified: shiro/trunk/core/src/main/java/org/apache/shiro/authc/credential/PasswordMatcher.java
URL: http://svn.apache.org/viewvc/shiro/trunk/core/src/main/java/org/apache/shiro/authc/credential/PasswordMatcher.java?rev=1213449&r1=1213448&r2=1213449&view=diff
==============================================================================
--- shiro/trunk/core/src/main/java/org/apache/shiro/authc/credential/PasswordMatcher.java (original)
+++ shiro/trunk/core/src/main/java/org/apache/shiro/authc/credential/PasswordMatcher.java Mon Dec 12 21:41:43 2011
@@ -20,7 +20,7 @@ package org.apache.shiro.authc.credentia
 
 import org.apache.shiro.authc.AuthenticationInfo;
 import org.apache.shiro.authc.AuthenticationToken;
-import org.apache.shiro.util.ByteSource;
+import org.apache.shiro.crypto.hash.Hash;
 
 /**
  * A {@link CredentialsMatcher} that employs best-practices comparisons for hashed text passwords.
@@ -36,10 +36,32 @@ public class PasswordMatcher implements 
     private PasswordService passwordService;
 
     public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
+
         PasswordService service = ensurePasswordService();
-        ByteSource submittedPassword = getSubmittedPassword(token);
-        String hashedPassword = getStoredHashedPassword(info);
-        return service.passwordsMatch(submittedPassword, hashedPassword);
+
+        Object submittedPassword = getSubmittedPassword(token);
+        Object storedCredentials = getStoredPassword(info);
+        assertStoredCredentialsType(storedCredentials);
+
+        if (storedCredentials instanceof Hash) {
+            Hash hashedPassword = (Hash)storedCredentials;
+            HashingPasswordService hashingService = assertHashingPasswordService(service);
+            return hashingService.passwordsMatch(submittedPassword, hashedPassword);
+        }
+        //otherwise they are a String (asserted in the 'assertStoredCredentialsType' method call above):
+        String formatted = (String)storedCredentials;
+        return passwordService.passwordsMatch(submittedPassword, formatted);
+    }
+
+    private HashingPasswordService assertHashingPasswordService(PasswordService service) {
+        if (service instanceof HashingPasswordService) {
+            return (HashingPasswordService) service;
+        }
+        String msg = "AuthenticationInfo's stored credentials are a Hash instance, but the " +
+                "configured passwordService is not a " +
+                HashingPasswordService.class.getName() + " instance.  This is required to perform Hash " +
+                "object password comparisons.";
+        throw new IllegalStateException(msg);
     }
 
     private PasswordService ensurePasswordService() {
@@ -51,24 +73,22 @@ public class PasswordMatcher implements 
         return service;
     }
 
-    protected ByteSource getSubmittedPassword(AuthenticationToken token) {
-        Object credentials = token.getCredentials();
-        if (credentials == null) {
-            return null;
-        }
-        return ByteSource.Util.bytes(credentials);
+    protected Object getSubmittedPassword(AuthenticationToken token) {
+        return token != null ? token.getCredentials() : null;
     }
 
-    protected String getStoredHashedPassword(AuthenticationInfo storedAccountInfo) {
-        Object credentials = storedAccountInfo.getCredentials();
-        if (credentials == null) {
-            return null;
-        }
-        if (!(credentials instanceof String)) {
-            String msg = "The stored account credentials is expected to be a String representation of a hashed password.";
-            throw new IllegalArgumentException(msg);
+    private void assertStoredCredentialsType(Object credentials) {
+        if (credentials instanceof String || credentials instanceof Hash) {
+            return;
         }
-        return (String)credentials;
+
+        String msg = "Stored account credentials are expected to be either a " +
+                Hash.class.getName() + " instance or a formatted hash String.";
+        throw new IllegalArgumentException(msg);
+    }
+
+    protected Object getStoredPassword(AuthenticationInfo storedAccountInfo) {
+        return storedAccountInfo != null ? storedAccountInfo.getCredentials() : null;
     }
 
     public PasswordService getPasswordService() {

Modified: shiro/trunk/core/src/main/java/org/apache/shiro/authc/credential/PasswordService.java
URL: http://svn.apache.org/viewvc/shiro/trunk/core/src/main/java/org/apache/shiro/authc/credential/PasswordService.java?rev=1213449&r1=1213448&r2=1213449&view=diff
==============================================================================
--- shiro/trunk/core/src/main/java/org/apache/shiro/authc/credential/PasswordService.java (original)
+++ shiro/trunk/core/src/main/java/org/apache/shiro/authc/credential/PasswordService.java Mon Dec 12 21:41:43 2011
@@ -34,16 +34,16 @@ import org.apache.shiro.util.ByteSource;
  * <h3>Account Creation or Password Reset</h3>
  * Whenever you create a new user account or reset that account's password, we must translate the end-user submitted
  * raw/plaintext password value to a string format that is much safer to store.  You do that by calling the
- * {@link #hashPassword(ByteSource)} method to create the safer hashed and formatted value.  For
+ * {@link #encryptPassword(Object)} method to create the safer value.  For
  * example:
  * <pre>
- * ByteSource plaintextBytes = ByteSource.Util.bytes(submittedPlaintextPassword);
- * String hashed = passwordService.hashPassword(plaintextBytes);
+ * String submittedPlaintextPassword = ...
+ * String encryptedValue = passwordService.encryptPassword(submittedPlaintextPassword);
  * ...
- * userAccount.setHashedPassword(hashed);
+ * userAccount.setPassword(encryptedValue);
  * userAccount.save(); //create or update to your data store
  * </pre>
- * Be sure to save this hashed password in your data store and never the original/raw submitted password.
+ * Be sure to save this encrypted password in your data store and never the original/raw submitted password.
  * <h3>Login Password Comparison</h3>
  * Shiro performs the comparison during login automatically.  Along with your {@code PasswordService}, you just
  * have to configure a {@link PasswordMatcher} on a realm that has password-based accounts.   During a login attempt,
@@ -55,7 +55,7 @@ import org.apache.shiro.util.ByteSource;
  * [main]
  * ...
  * passwordService = org.apache.shiro.authc.credential.DefaultPasswordService
- * # configure the passwordService to use the hashing settings you desire
+ * # configure the passwordService to use the settings you desire
  * ...
  * passwordMatcher = org.apache.shiro.authc.credential.PasswordMatcher
  * passwordMatcher.passwordService = $passwordService
@@ -72,65 +72,76 @@ import org.apache.shiro.util.ByteSource;
 public interface PasswordService {
 
     /**
-     * Hashes the specified plaintext password (usually acquired from your application's 'new user' or 'password reset'
-     * workflow).  After this call returns, you typically will store the returned formatted String with the
-     * corresponding user record (e.g. as the 'password' or 'passwordHash' attribute).
+     * Converts the specified plaintext password (usually acquired from your application's 'new user' or 'password reset'
+     * workflow) into a formatted string safe for storage.  The returned string can be safely saved with the
+     * corresponding user account record (e.g. as a 'password' attribute).
      * <p/>
-     * The String returned from this argument must be presented to the
-     * {@link #passwordsMatch(ByteSource, String) passwordsMatch} method when performing a
+     * It is expected that the String returned from this method will be presented to the
+     * {@link #passwordsMatch(Object, String) passwordsMatch(plaintext,encrypted)} method when performing a
      * password comparison check.
      * <h3>Usage</h3>
-     * The input argument type is a {@code ByteSource} to support either String or character array
-     * {@code (char[])} arguments; character arrays are often a safer way to represent passwords as they can be
-     * cleared/nulled-out after use.
+     * The input argument type can be any 'byte backed' {@code Object} - almost always either a
+     * String or character array representing passwords (character arrays are often a safer way to represent passwords
+     * as they can be cleared/nulled-out after use.  Any argument type supported by
+     * {@link ByteSource.Util#isCompatible(Object)} is valid.
      * <p/>
-     * Regardless of your choice of using Strings or character arrays to represent submitted passwords, you can wrap
-     * either as a {@code ByteSource} by using {@link ByteSource.Util}, for example, when the passwords are captured as
-     * Strings:
+     * For example:
      * <pre>
-     * ByteSource passwordBytes = ByteSource.Util.bytes(submittedPasswordString);
-     * String formattedHashedValue = passwordService.hashPassword(passwordBytes);
+     * String rawPassword = ...
+     * String encryptedValue = passwordService.encryptPassword(rawPassword);
      * </pre>
-     * or, identically, when captured as a character array:
+     * or, identically:
      * <pre>
-     * ByteSource passwordBytes = ByteSource.Util.bytes(submittedPasswordCharacterArray);
-     * String formattedHashedValue = passwordService.hashPassword(passwordBytes);
+     * char[] rawPasswordChars = ...
+     * String encryptedValue = passwordService.encryptPassword(rawPasswordChars);
      * </pre>
      * <p/>
-     * The resulting {@code formattedHashedValue} should be stored with the account to be retrieved later during a
+     * The resulting {@code encryptedValue} should be stored with the account to be retrieved later during a
      * login attempt.  For example:
      * <pre>
-     * String formattedHashedValue = passwordService.hashPassword(passwordBytes);
+     * String encryptedValue = passwordService.encryptPassword(rawPassword);
      * ...
-     * userAccount.setHashedPassword(formattedHashedValue);
+     * userAccount.setPassword(encryptedValue);
      * userAccount.save(); //create or update to your data store
      * </pre>
      *
-     * @param plaintext a {@code ByteSource} encapsulating a plaintext password's bytes, usually acquired from your
-     *                  application's 'new user' or 'password reset' workflow.
-     * @return the hashed password, formatted for storage.
+     * @param plaintextPassword the raw password as 'byte-backed' object (String, character array, {@link ByteSource},
+     *                          etc) usually acquired from your application's 'new user' or 'password reset' workflow.
+     * @return the encrypted password, formatted for storage.
+     * @throws IllegalArgumentException if the argument cannot be easily converted to bytes as defined by
+     *                                  {@link ByteSource.Util#isCompatible(Object)}.
+     * @see ByteSource.Util#isCompatible(Object)
      */
-    String hashPassword(ByteSource plaintext);
+    String encryptPassword(Object plaintextPassword) throws IllegalArgumentException;
 
     /**
      * Returns {@code true} if the {@code submittedPlaintext} password matches the existing {@code saved} password,
      * {@code false} otherwise.
      * <h3>Usage</h3>
-     * The {@code submittedPlaintext} argument is a {@code ByteSource} to support both String and character array
-     * arguments.  Regardless of which you use to capture submitted passwords, you can wrap either as a
-     * {@code ByteSource} as follows:
+     * The {@code submittedPlaintext} argument type can be any 'byte backed' {@code Object} - almost always either a
+     * String or character array representing passwords (character arrays are often a safer way to represent passwords
+     * as they can be cleared/nulled-out after use.  Any argument type supported by
+     * {@link ByteSource.Util#isCompatible(Object)} is valid.
+     * <p/>
+     * For example:
+     * <pre>
+     * String submittedPassword = ...
+     * passwordService.passwordsMatch(submittedPassword, encryptedPassword);
+     * </pre>
+     * or similarly:
      * <pre>
-     * ByteSource submittedPasswordBytes = ByteSource.Util.bytes(submittedPasswordStringOrCharacterArray);
-     * passwordService.passwordsMatch(submittedPasswordBytes, formattedHashedPassword);
+     * char[] submittedPasswordCharacters = ...
+     * passwordService.passwordsMatch(submittedPasswordCharacters, encryptedPassword);
      * </pre>
      *
      * @param submittedPlaintext a raw/plaintext password submitted by an end user/Subject.
-     * @param saved              the previously hashed and formatted password known to be associated with an account.
-     *                           This value must have been previously generated from the
-     *                           {@link #hashPassword(ByteSource) hashPassword} method (typically
+     * @param encrypted          the previously encrypted password known to be associated with an account.
+     *                           This value is expected to have been previously generated from the
+     *                           {@link #encryptPassword(Object) encryptPassword} method (typically
      *                           when the account is created or the account's password is reset).
      * @return {@code true} if the {@code submittedPlaintext} password matches the existing {@code saved} password,
      *         {@code false} otherwise.
+     * @see ByteSource.Util#isCompatible(Object)
      */
-    boolean passwordsMatch(ByteSource submittedPlaintext, String saved);
+    boolean passwordsMatch(Object submittedPlaintext, String encrypted);
 }

Modified: shiro/trunk/core/src/main/java/org/apache/shiro/crypto/hash/DefaultHashService.java
URL: http://svn.apache.org/viewvc/shiro/trunk/core/src/main/java/org/apache/shiro/crypto/hash/DefaultHashService.java?rev=1213449&r1=1213448&r2=1213449&view=diff
==============================================================================
--- shiro/trunk/core/src/main/java/org/apache/shiro/crypto/hash/DefaultHashService.java (original)
+++ shiro/trunk/core/src/main/java/org/apache/shiro/crypto/hash/DefaultHashService.java Mon Dec 12 21:41:43 2011
@@ -26,6 +26,27 @@ import org.apache.shiro.util.ByteSource;
  * Default implementation of the {@link HashService} interface, supporting a customizable hash algorithm name,
  * secure-random salt generation, multiple hash iterations and an optional internal
  * {@link #setPrivateSalt(ByteSource) privateSalt}.
+ * <h2>Hash Algorithm</h2>
+ * You may specify a hash algorithm via the {@link #setHashAlgorithmName(String)} property.  Any algorithm name
+ * understood by the JDK
+ * {@link java.security.MessageDigest#getInstance(String) MessageDigest.getInstance(String algorithmName)} method
+ * will work.  The default is {@code SHA-512}.
+ * <h2>Random Salts</h2>
+ * When a salt is not specified in a request, this implementation generates secure random salts via its
+ * {@link #setRandomNumberGenerator(org.apache.shiro.crypto.RandomNumberGenerator) randomNumberGenerator} property.
+ * Random salts (and potentially combined with the internal {@link #getPrivateSalt() privateSalt}) is a very strong
+ * salting strategy, as salts should ideally never be based on known/guessable data.  The default instance is a
+ * {@link SecureRandomNumberGenerator}.
+ * <h2>Hash Iterations</h2>
+ * Secure hashing strategies often employ multiple hash iterations to slow down the hashing process.  This technique
+ * is usually used for password hashing, since the longer it takes to compute a password hash, the longer it would
+ * take for an attacker to compromise a password.  This
+ * <a href="http://www.katasoft.com/blog/2011/04/04/strong-password-hashing-apache-shiro">Katasoft blog article</a>
+ * explains in greater detail why this is useful, as well as information on how many iterations is 'enough'.
+ * <p/>
+ * You may set the number of hash iterations via the {@link #setHashIterations(int)} property.  The default is
+ * {@code 1}, but should be increased significantly if the {@code HashService} is intended to be used for password
+ * hashing. See the linked blog article for more info.
  * <h2>Private Salt</h2>
  * If using this implementation as part of a password hashing strategy, it might be desirable to configure a
  * {@link #setPrivateSalt(ByteSource) private salt}:
@@ -42,27 +63,6 @@ import org.apache.shiro.util.ByteSource;
  * <p/>
  * <b>*</b>By default, the {@link #getPrivateSalt() privateSalt} is null, since a sensible default cannot be used that
  * isn't easily compromised (because Shiro is an open-source project and any default could be easily seen and used).
- * <h2>Random Salts</h2>
- * When a salt is not specified in a request, this implementation generates secure random salts via its
- * {@link #setRandomNumberGenerator(org.apache.shiro.crypto.RandomNumberGenerator) randomNumberGenerator} property.
- * Random salts (and potentially combined with the internal {@link #getPrivateSalt() privateSalt}) is a very strong
- * salting strategy, as salts should ideally never be based on known/guessable data.  The default instance is a
- * {@link SecureRandomNumberGenerator}.
- * <h2>Password Hash Iterations</h2>
- * Secure hashing strategies often employ multiple hash iterations to slow down the hashing process.  This technique
- * is usually used for password hashing, since the longer it takes to compute a password hash, the longer it would
- * take for an attacker to compromise a password.  This
- * <a href="http://www.katasoft.com/blog/2011/04/04/strong-password-hashing-apache-shiro">Katasoft blog article</a>
- * explains in greater detail why this is useful, as well as information on how many iterations is 'enough'.
- * <p/>
- * You may set the number of hash iterations via the {@link #setHashIterations(int)} property.  The default is
- * {@code 1}, but should be increased significantly if the {@code HashService} is intended to be used for password
- * hashing. See the linked blog article for more info.
- * <h2>Hash Algorithm</h2>
- * You may specify a hash algorithm via the {@link #setHashAlgorithmName(String)} property.  Any algorithm name
- * understood by the JDK
- * {@link java.security.MessageDigest#getInstance(String) MessageDigest.getInstance(String algorithmName)} method
- * will work.  The default is {@code SHA-512}.
  *
  * @since 1.2
  */
@@ -148,42 +148,44 @@ public class DefaultHashService implemen
      *         exposed to the caller.
      */
     public Hash computeHash(HashRequest request) {
-        if (request == null) {
+        if (request == null || request.getSource() == null || request.getSource().isEmpty()) {
             return null;
         }
-        ByteSource source = request.getSource();
 
-        byte[] sourceBytes = source != null ? source.getBytes() : null;
-        if (sourceBytes == null || sourceBytes.length == 0) {
-            return null;
-        }
-
-        ByteSource requestSalt = request.getSalt();
-        ByteSource publicSalt = requestSalt != null ? requestSalt : null;
-        if (requestSalt != null && (requestSalt.getBytes() == null || requestSalt.getBytes().length == 0)) {
-            publicSalt = null;
-        }
-
-        if (publicSalt == null) {
-            publicSalt = getRandomNumberGenerator().nextBytes();
-        }
+        String algorithmName = getAlgorithmName(request);
+        ByteSource source = request.getSource();
+        int iterations = getIterations(request);
 
-        String algorithmName = getHashAlgorithmName();
+        ByteSource publicSalt = getPublicSalt(request);
         ByteSource privateSalt = getPrivateSalt();
-        ByteSource combinedSalt = combine(privateSalt, publicSalt);
-        int iterations = Math.max(1, getHashIterations());
+        ByteSource salt = combine(privateSalt, publicSalt);
 
-        Hash computed = new SimpleHash(algorithmName, sourceBytes, combinedSalt, iterations);
+        Hash computed = new SimpleHash(algorithmName, source, salt, iterations);
 
         SimpleHash result = new SimpleHash(algorithmName);
         result.setBytes(computed.getBytes());
         result.setIterations(iterations);
-        //Only expose the public salt - not the real/combined salt that was used:
+        //Only expose the public salt - not the real/combined salt that might have been used:
         result.setSalt(publicSalt);
 
         return result;
     }
 
+    protected String getAlgorithmName(HashRequest request) {
+        String name = request.getAlgorithmName();
+        if (name == null) {
+            name = getHashAlgorithmName();
+        }
+        return name;
+    }
+
+    protected int getIterations(HashRequest request) {
+        int iterations = Math.max(0, request.getIterations());
+        if (iterations < 1) {
+            iterations = Math.max(1, getHashIterations());
+        }
+        return iterations;
+    }
 
     /**
      * Returns the public salt that should be used to compute a hash based on the specified request or

Modified: shiro/trunk/core/src/main/java/org/apache/shiro/crypto/hash/format/HashFormatFactory.java
URL: http://svn.apache.org/viewvc/shiro/trunk/core/src/main/java/org/apache/shiro/crypto/hash/format/HashFormatFactory.java?rev=1213449&r1=1213448&r2=1213449&view=diff
==============================================================================
--- shiro/trunk/core/src/main/java/org/apache/shiro/crypto/hash/format/HashFormatFactory.java (original)
+++ shiro/trunk/core/src/main/java/org/apache/shiro/crypto/hash/format/HashFormatFactory.java Mon Dec 12 21:41:43 2011
@@ -23,5 +23,5 @@ package org.apache.shiro.crypto.hash.for
  */
 public interface HashFormatFactory {
 
-    HashFormat getInstance(String id);
+    HashFormat getInstance(String token);
 }

Modified: shiro/trunk/core/src/test/groovy/org/apache/shiro/authc/credential/DefaultPasswordServiceTest.groovy
URL: http://svn.apache.org/viewvc/shiro/trunk/core/src/test/groovy/org/apache/shiro/authc/credential/DefaultPasswordServiceTest.groovy?rev=1213449&r1=1213448&r2=1213449&view=diff
==============================================================================
--- shiro/trunk/core/src/test/groovy/org/apache/shiro/authc/credential/DefaultPasswordServiceTest.groovy (original)
+++ shiro/trunk/core/src/test/groovy/org/apache/shiro/authc/credential/DefaultPasswordServiceTest.groovy Mon Dec 12 21:41:43 2011
@@ -1,25 +1,143 @@
+/*
+ * 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.shiro.authc.credential
 
-import org.apache.shiro.util.ByteSource
+import org.apache.shiro.crypto.SecureRandomNumberGenerator
+import org.apache.shiro.crypto.hash.format.HashFormatFactory
+import org.apache.shiro.crypto.hash.format.HexFormat
+import org.apache.shiro.crypto.hash.format.Shiro1CryptFormat
+import org.apache.shiro.crypto.hash.*
+import static org.easymock.EasyMock.*
 
 /**
- * Created by IntelliJ IDEA.
- * User: lhazlewood
- * Date: 9/20/11
- * Time: 3:38 PM
- * To change this template use File | Settings | File Templates.
+ * Unit tests for the {@link DefaultPasswordService} implementation.
+ *
+ * @since 1.2
  */
 class DefaultPasswordServiceTest extends GroovyTestCase {
 
-    void testDefault() {
-        def passwordService = new DefaultPasswordService();
+    void testEncryptPasswordWithNullArgument() {
+        def service = new DefaultPasswordService()
+        assertNull service.encryptPassword(null)
+    }
+
+    void testHashPasswordWithNullArgument() {
+        def service = new DefaultPasswordService()
+        assertNull service.hashPassword(null)
+    }
+
+    void testEncryptPasswordDefault() {
+        def service = new DefaultPasswordService()
+        def encrypted = service.encryptPassword("12345")
+        assertTrue service.passwordsMatch("12345", encrypted)
+    }
+
+    void testEncryptPasswordWithInvalidMatch() {
+        def service = new DefaultPasswordService()
+        def encrypted = service.encryptPassword("ABCDEF")
+        assertFalse service.passwordsMatch("ABC", encrypted)
+    }
+
+    void testBackwardsCompatibility() {
+        def service = new DefaultPasswordService()
+        def encrypted = service.encryptPassword("12345")
+        def submitted = "12345"
+        assertTrue service.passwordsMatch(submitted, encrypted);
+
+        //change some settings:
+        service.hashService.hashAlgorithmName = "MD5"
+        service.hashService.hashIterations = 250000
+
+        def encrypted2 = service.encryptPassword(submitted)
+
+        assertFalse encrypted == encrypted2
+
+        assertTrue service.passwordsMatch(submitted, encrypted2)
+    }
+
+    void testHashFormatWarned() {
+        def service = new DefaultPasswordService()
+        service.hashFormat = new HexFormat()
+        assertTrue service.hashFormat instanceof HexFormat
+        service.encryptPassword("test")
+        assertTrue service.hashFormatWarned
+    }
+
+    void testPasswordsMatchWithNullOrEmpty() {
+        def service = new DefaultPasswordService()
+        assertTrue service.passwordsMatch(null, (String)null)
+        assertTrue service.passwordsMatch(null, (Hash)null)
+        assertTrue service.passwordsMatch("", (String)null)
+        assertTrue service.passwordsMatch(null, "")
+        assertFalse service.passwordsMatch(null, "12345")
+        assertFalse service.passwordsMatch(null, new Sha1Hash("test"))
+    }
+
+    void testCustomHashService() {
+        def hashService = createMock(HashService)
+
+        def hash = new Sha256Hash("test", new SecureRandomNumberGenerator().nextBytes(), 100);
+
+        expect(hashService.computeHash(isA(HashRequest))).andReturn hash
+
+        replay hashService
+
+        def service = new DefaultPasswordService()
+        service.hashService = hashService
+
+        def returnedHash = service.encryptPassword("test")
+
+        assertEquals new Shiro1CryptFormat().format(hash), returnedHash
+
+        verify hashService
+    }
+
+    void testCustomHashFormatFactory() {
+
+        def factory = createMock(HashFormatFactory)
+        def hash = new Sha512Hash("test", new SecureRandomNumberGenerator().nextBytes(), 100)
+        String saved = new Shiro1CryptFormat().format(hash)
+
+        expect(factory.getInstance(eq(saved))).andReturn(new Shiro1CryptFormat())
+
+        replay factory
+
+        def service = new DefaultPasswordService()
+        service.hashFormatFactory = factory
+
+        assertSame factory, service.hashFormatFactory
+
+        assertTrue service.passwordsMatch("test", saved)
+
+        verify factory
+    }
+
+    void testStringComparisonWhenNotUsingAParsableHashFormat() {
 
-        def password = ByteSource.Util.bytes("12345")
+        def service = new DefaultPasswordService()
+        service.hashFormat = new HexFormat()
+        //can't use random salts when using HexFormat:
+        service.hashService.generatePublicSalt = false
 
-        def formatted = passwordService.hashPassword(password)
-        System.out.println "Formatted/stored password: $formatted"
+        def formatted = service.encryptPassword("12345")
 
-        assertTrue passwordService.passwordsMatch(password, '$shiro1$SHA-512$300000$d07mwTTz3EHqQEdc5KBPCgzigcuwYmbfD3nw7ao7zmA=$B76M6PRqOl4kaScZjKHDWVcE08MwOrqTQyqmmPAIw9Sl0ONG/Rv7GxeUfc5fA3ujhxKJgGgDllDC1EchHFlncw==');
+        assertTrue service.passwordsMatch("12345", formatted)
     }
 
 }

Added: shiro/trunk/core/src/test/groovy/org/apache/shiro/authc/credential/PasswordMatcherTest.groovy
URL: http://svn.apache.org/viewvc/shiro/trunk/core/src/test/groovy/org/apache/shiro/authc/credential/PasswordMatcherTest.groovy?rev=1213449&view=auto
==============================================================================
--- shiro/trunk/core/src/test/groovy/org/apache/shiro/authc/credential/PasswordMatcherTest.groovy (added)
+++ shiro/trunk/core/src/test/groovy/org/apache/shiro/authc/credential/PasswordMatcherTest.groovy Mon Dec 12 21:41:43 2011
@@ -0,0 +1,143 @@
+/*
+ * 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.shiro.authc.credential
+
+import org.apache.shiro.authc.AuthenticationInfo
+import org.apache.shiro.authc.AuthenticationToken
+import org.apache.shiro.crypto.hash.Sha256Hash
+import static org.easymock.EasyMock.*
+
+/**
+ * Unit tests for the {@link PasswordMatcher} implementation.
+ *
+ * @since 1.2
+ */
+class PasswordMatcherTest extends GroovyTestCase {
+
+    void testMissingPasswordService() {
+        def matcher = new PasswordMatcher()
+        try {
+            matcher.doCredentialsMatch(null, null)
+            fail "Test should fail due to lack of a configured PasswordService instance."
+        } catch (IllegalStateException expected) {
+        }
+    }
+
+    void testStringPasswordComparison() {
+        def service = createMock(PasswordService)
+        def token = createMock(AuthenticationToken)
+        def info = createMock(AuthenticationInfo)
+        //generate a stored password just for this test:
+        def submittedPassword = "plaintext"
+        def savedPassword = "encrypted"
+
+        expect(token.credentials).andReturn submittedPassword
+        expect(info.credentials).andReturn savedPassword
+
+        expect(service.passwordsMatch(eq(submittedPassword), eq(savedPassword))).andReturn true
+
+        replay token, info, service
+
+        def matcher = new PasswordMatcher()
+        matcher.passwordService = service
+        assertSame service, matcher.passwordService
+
+        assertTrue matcher.doCredentialsMatch(token, info)
+
+        verify token, info, service
+    }
+
+    void testHashComparisonWithoutHashedPasswordService() {
+        def service = createMock(PasswordService)
+        def token = createMock(AuthenticationToken)
+        def info = createMock(AuthenticationInfo)
+        //generate a stored password just for this test:
+        def submittedPassword = "plaintext"
+        def savedPassword = new Sha256Hash("plaintext")
+
+        expect(token.credentials).andReturn submittedPassword
+        expect(info.credentials).andReturn savedPassword
+
+        replay token, info, service
+
+        def matcher = new PasswordMatcher()
+        matcher.passwordService = service
+        assertSame service, matcher.passwordService
+
+        try {
+            assertTrue matcher.doCredentialsMatch(token, info)
+            fail "matcher should fail since PasswordService is not a HashingPasswordService"
+        } catch (IllegalStateException expected) {
+        }
+
+        verify token, info, service
+    }
+
+    void testHashComparison() {
+        def service = createMock(HashingPasswordService)
+        def token = createMock(AuthenticationToken)
+        def info = createMock(AuthenticationInfo)
+        //generate a stored password just for this test:
+        def submittedPassword = "plaintext"
+        def savedPassword = new Sha256Hash("plaintext")
+
+        expect(token.credentials).andReturn submittedPassword
+        expect(info.credentials).andReturn savedPassword
+
+        expect(service.passwordsMatch(submittedPassword, savedPassword)).andReturn true
+
+        replay token, info, service
+
+        def matcher = new PasswordMatcher()
+        matcher.passwordService = service
+        assertSame service, matcher.passwordService
+
+        assertTrue matcher.doCredentialsMatch(token, info)
+
+        verify token, info, service
+    }
+
+    void testUnexpectedSavedCredentialsType() {
+        def service = createMock(HashingPasswordService)
+        def token = createMock(AuthenticationToken)
+        def info = createMock(AuthenticationInfo)
+        //generate a stored password just for this test:
+        def submittedPassword = "plaintext"
+        def savedPassword = 23
+
+        expect(token.credentials).andReturn submittedPassword
+        expect(info.credentials).andReturn savedPassword
+
+        replay token, info, service
+
+        def matcher = new PasswordMatcher()
+        matcher.passwordService = service
+        assertSame service, matcher.passwordService
+
+        try {
+            assertTrue matcher.doCredentialsMatch(token, info)
+            fail "Saved credentials should be either a String or Hash instance."
+        } catch (IllegalArgumentException expected) {
+        }
+
+        verify token, info, service
+
+    }
+
+}

Added: shiro/trunk/core/src/test/groovy/org/apache/shiro/crypto/hash/DefaultHashServiceTest.groovy
URL: http://svn.apache.org/viewvc/shiro/trunk/core/src/test/groovy/org/apache/shiro/crypto/hash/DefaultHashServiceTest.groovy?rev=1213449&view=auto
==============================================================================
--- shiro/trunk/core/src/test/groovy/org/apache/shiro/crypto/hash/DefaultHashServiceTest.groovy (added)
+++ shiro/trunk/core/src/test/groovy/org/apache/shiro/crypto/hash/DefaultHashServiceTest.groovy Mon Dec 12 21:41:43 2011
@@ -0,0 +1,159 @@
+/*
+ * 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.shiro.crypto.hash
+
+import org.apache.shiro.crypto.RandomNumberGenerator
+import org.apache.shiro.crypto.SecureRandomNumberGenerator
+import org.apache.shiro.util.ByteSource
+import static org.easymock.EasyMock.*
+
+/**
+ * Unit tests for the {@link DefaultHashService} implementation.
+ *
+ * @since 1.2
+ */
+class DefaultHashServiceTest extends GroovyTestCase {
+
+    void testNullRequest() {
+        assertNull createService().computeHash(null)
+    }
+
+    void testDifferentAlgorithmName() {
+        def service = new DefaultHashService(hashAlgorithmName: 'MD5')
+        def hash = hash(service, "test")
+        assertEquals 'MD5', hash.algorithmName
+    }
+
+    void testDifferentIterations() {
+        def service = new DefaultHashService(hashIterations: 2)
+        def hash = hash(service, "test")
+        assertEquals 2, hash.iterations
+    }
+
+    void testDifferentRandomNumberGenerator() {
+
+        def ByteSource randomBytes = new SecureRandomNumberGenerator().nextBytes()
+        def rng = createMock(RandomNumberGenerator)
+        expect(rng.nextBytes()).andReturn randomBytes
+
+        replay rng
+
+        def service = new DefaultHashService(randomNumberGenerator: rng, generatePublicSalt: true)
+        hash(service, "test")
+
+        verify rng
+    }
+
+    /**
+     * If 'generatePublicSalt' is true, 2 hashes of the same input source should be different.
+     */
+    void testWithRandomlyGeneratedSalt() {
+        def service = new DefaultHashService(generatePublicSalt: true)
+        def first = hash(service, "password")
+        def second = hash(service, "password")
+        assertFalse first == second
+    }
+
+    void testRequestWithEmptySource() {
+        def source = ByteSource.Util.bytes((byte[])null)
+        def request = new HashRequest.Builder().setSource(source).build()
+        def service = createService()
+        assertNull service.computeHash(request)
+    }
+
+    /**
+     * Two different strings hashed with the same salt should result in two different
+     * hashes.
+     */
+    void testOnlyRandomSaltHash() {
+        HashService service = createService();
+        Hash first = hash(service, "password");
+        Hash second = hash(service, "password2", first.salt);
+        assertFalse first == second
+    }
+
+    /**
+     * If the same string is hashed twice and only base salt was supplied, hashed
+     * result should be different in each case.
+     */
+    void testBothSaltsRandomness() {
+        HashService service = createServiceWithPrivateSalt();
+        Hash first = hash(service, "password");
+        Hash second = hash(service, "password");
+        assertFalse first == second
+    }
+
+    /**
+     * If a string is hashed and only base salt was supplied, random salt is generated.
+     * Hash of the same string with generated random salt should return the
+     * same result.
+     */
+    void testBothSaltsReturn() {
+        HashService service = createServiceWithPrivateSalt();
+        Hash first = hash(service, "password");
+        Hash second = hash(service, "password", first.salt);
+        assertEquals first, second
+    }
+
+    /**
+     * Two different strings hashed with the same salt should result in two different
+     * hashes.
+     */
+    void testBothSaltsHash() {
+        HashService service = createServiceWithPrivateSalt();
+        Hash first = hash(service, "password");
+        Hash second = hash(service, "password2", first.salt);
+        assertFalse first == second
+    }
+
+    /**
+     * Hash result is different if the base salt is added.
+     */
+    public void testPrivateSaltChangesResult() {
+        HashService saltedService = createServiceWithPrivateSalt();
+        HashService service = createService();
+        Hash first = hashPredictable(saltedService, "password");
+        Hash second = hashPredictable(service, "password");
+        assertFalse first == second
+    }
+
+    protected Hash hash(HashService hashService, def source) {
+        return hashService.computeHash(new HashRequest.Builder().setSource(source).build());
+    }
+
+    protected Hash hash(HashService hashService, def source, def salt) {
+        return hashService.computeHash(new HashRequest.Builder().setSource(source).setSalt(salt).build());
+    }
+
+    private Hash hashPredictable(HashService hashService, def source) {
+        byte[] salt = new byte[20];
+        Arrays.fill(salt, (byte) 2);
+        return hashService.computeHash(new HashRequest.Builder().setSource(source).setSalt(salt).build());
+    }
+
+    private DefaultHashService createService() {
+        return new DefaultHashService();
+    }
+
+    private DefaultHashService createServiceWithPrivateSalt() {
+        DefaultHashService defaultHashService = new DefaultHashService();
+        defaultHashService.setPrivateSalt(new SecureRandomNumberGenerator().nextBytes());
+        return defaultHashService;
+    }
+}

Added: shiro/trunk/core/src/test/groovy/org/apache/shiro/crypto/hash/HashRequestBuilderTest.groovy
URL: http://svn.apache.org/viewvc/shiro/trunk/core/src/test/groovy/org/apache/shiro/crypto/hash/HashRequestBuilderTest.groovy?rev=1213449&view=auto
==============================================================================
--- shiro/trunk/core/src/test/groovy/org/apache/shiro/crypto/hash/HashRequestBuilderTest.groovy (added)
+++ shiro/trunk/core/src/test/groovy/org/apache/shiro/crypto/hash/HashRequestBuilderTest.groovy Mon Dec 12 21:41:43 2011
@@ -0,0 +1,58 @@
+/*
+ * 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.shiro.crypto.hash
+
+import org.apache.shiro.crypto.SecureRandomNumberGenerator
+import org.apache.shiro.util.ByteSource
+
+/**
+ * Unit tests for the {@link HashRequest.Builder} implementation
+ *
+ * @since 1.2
+ */
+class HashRequestBuilderTest extends GroovyTestCase {
+
+    void testNullSource() {
+        try {
+            new HashRequest.Builder().build()
+            fail "NullPointerException should be thrown"
+        } catch (NullPointerException expected) {
+        }
+    }
+
+    void testDefault() {
+        assertEquals 0, new HashRequest.Builder().setSource("test").build().iterations
+    }
+
+    void testConfig() {
+        ByteSource source = ByteSource.Util.bytes("test")
+        ByteSource salt = new SecureRandomNumberGenerator().nextBytes()
+        def request = new HashRequest.Builder()
+            .setSource(source)
+            .setSalt(salt)
+            .setIterations(2)
+            .setAlgorithmName('MD5').build()
+
+        assertNotNull request
+        assertEquals source, request.source
+        assertEquals salt, request.salt
+        assertEquals 2, request.iterations
+        assertEquals 'MD5', request.algorithmName
+    }
+}



Mime
View raw message