فهرست منبع

Add crypto PasswordEncoder from 5.0.x

Fixes: gh-5302
Rob Winch 7 سال پیش
والد
کامیت
0f612bf637
30فایلهای تغییر یافته به همراه2114 افزوده شده و 9 حذف شده
  1. 3 0
      core/src/main/java/org/springframework/security/authentication/encoding/BaseDigestPasswordEncoder.java
  2. 3 0
      core/src/main/java/org/springframework/security/authentication/encoding/BasePasswordEncoder.java
  3. 2 0
      core/src/main/java/org/springframework/security/authentication/encoding/LdapShaPasswordEncoder.java
  4. 3 0
      core/src/main/java/org/springframework/security/authentication/encoding/Md4PasswordEncoder.java
  5. 4 0
      core/src/main/java/org/springframework/security/authentication/encoding/Md5PasswordEncoder.java
  6. 3 0
      core/src/main/java/org/springframework/security/authentication/encoding/MessageDigestPasswordEncoder.java
  7. 3 0
      core/src/main/java/org/springframework/security/authentication/encoding/PlaintextPasswordEncoder.java
  8. 4 0
      core/src/main/java/org/springframework/security/authentication/encoding/ShaPasswordEncoder.java
  9. 79 0
      crypto/src/main/java/org/springframework/security/crypto/factory/PasswordEncoderFactories.java
  10. 79 0
      crypto/src/main/java/org/springframework/security/crypto/keygen/Base64StringKeyGenerator.java
  11. 241 0
      crypto/src/main/java/org/springframework/security/crypto/password/DelegatingPasswordEncoder.java
  12. 9 2
      crypto/src/main/java/org/springframework/security/crypto/password/Digester.java
  13. 211 0
      crypto/src/main/java/org/springframework/security/crypto/password/LdapShaPasswordEncoder.java
  14. 182 0
      crypto/src/main/java/org/springframework/security/crypto/password/Md4.java
  15. 155 0
      crypto/src/main/java/org/springframework/security/crypto/password/Md4PasswordEncoder.java
  16. 174 0
      crypto/src/main/java/org/springframework/security/crypto/password/MessageDigestPasswordEncoder.java
  17. 8 0
      crypto/src/main/java/org/springframework/security/crypto/password/NoOpPasswordEncoder.java
  18. 58 0
      crypto/src/main/java/org/springframework/security/crypto/password/PasswordEncoderUtils.java
  19. 67 5
      crypto/src/main/java/org/springframework/security/crypto/password/Pbkdf2PasswordEncoder.java
  20. 9 0
      crypto/src/main/java/org/springframework/security/crypto/password/StandardPasswordEncoder.java
  21. 1 1
      crypto/src/test/java/org/springframework/security/crypto/codec/HexTests.java
  22. 101 0
      crypto/src/test/java/org/springframework/security/crypto/factory/PasswordEncoderFactoriesTests.java
  23. 66 0
      crypto/src/test/java/org/springframework/security/crypto/keygen/Base64StringKeyGeneratorTests.java
  24. 219 0
      crypto/src/test/java/org/springframework/security/crypto/password/DelegatingPasswordEncoderTests.java
  25. 111 0
      crypto/src/test/java/org/springframework/security/crypto/password/LdapShaPasswordEncoderTests.java
  26. 76 0
      crypto/src/test/java/org/springframework/security/crypto/password/Md4PasswordEncoderTests.java
  27. 121 0
      crypto/src/test/java/org/springframework/security/crypto/password/MessageDigestPasswordEncoderTests.java
  28. 70 0
      crypto/src/test/java/org/springframework/security/crypto/password/PasswordEncoderUtilsTests.java
  29. 37 1
      crypto/src/test/java/org/springframework/security/crypto/password/Pbkdf2PasswordEncoderTests.java
  30. 15 0
      crypto/src/test/resources/logback-test.xml

+ 3 - 0
core/src/main/java/org/springframework/security/authentication/encoding/BaseDigestPasswordEncoder.java

@@ -22,7 +22,10 @@ package org.springframework.security.authentication.encoding;
  * </p>
  *
  * @author colin sampaleanu
+ * @deprecated This is deprecated and marked for deletion. Replace with an implementation
+ * of {@link org.springframework.security.crypto.password.PasswordEncoder}
  */
+@Deprecated
 public abstract class BaseDigestPasswordEncoder extends BasePasswordEncoder {
 	// ~ Instance fields
 	// ================================================================================================

+ 3 - 0
core/src/main/java/org/springframework/security/authentication/encoding/BasePasswordEncoder.java

@@ -22,7 +22,10 @@ package org.springframework.security.authentication.encoding;
  * </p>
  *
  * @author Ben Alex
+ * @deprecated This is deprecated and marked for deletion. Replace with an implementation
+ * of {@link org.springframework.security.crypto.password.PasswordEncoder}
  */
+@Deprecated
 public abstract class BasePasswordEncoder implements PasswordEncoder {
 	// ~ Methods
 	// ========================================================================================================

+ 2 - 0
core/src/main/java/org/springframework/security/authentication/encoding/LdapShaPasswordEncoder.java

@@ -32,6 +32,8 @@ import org.springframework.util.Assert;
  * and non-encoded passwords are in use or when a null implementation is required.
  *
  * @author Luke Taylor
+ * @deprecated @deprecated This is deprecated and marked for deletion. Replace with
+ * of {@link org.springframework.security.crypto.password.LdapShaPasswordEncoder}
  */
 public class LdapShaPasswordEncoder implements PasswordEncoder {
 	// ~ Static fields/initializers

+ 3 - 0
core/src/main/java/org/springframework/security/authentication/encoding/Md4PasswordEncoder.java

@@ -31,7 +31,10 @@ import org.springframework.security.crypto.codec.Utf8;
  * legacy applications, it's not secure, don't use it for anything new!
  *
  * @author Alan Stewart
+ * @deprecated This is deprecated and marked for deletion. Replace with an implementation
+ * of {@link org.springframework.security.crypto.password.Md4PasswordEncoder}
  */
+@Deprecated
 public class Md4PasswordEncoder extends BaseDigestPasswordEncoder {
 
 	// ~ Methods

+ 4 - 0
core/src/main/java/org/springframework/security/authentication/encoding/Md5PasswordEncoder.java

@@ -33,7 +33,11 @@ package org.springframework.security.authentication.encoding;
  * @author Ray Krueger
  * @author colin sampaleanu
  * @author Ben Alex
+ * @deprecated This is deprecated and marked for deletion. Replace with an implementation
+ * of {@link org.springframework.security.crypto.password.MessageDigestPasswordEncoder}
+ * with an algorithm of "MD5"
  */
+@Deprecated
 public class Md5PasswordEncoder extends MessageDigestPasswordEncoder {
 
 	public Md5PasswordEncoder() {

+ 3 - 0
core/src/main/java/org/springframework/security/authentication/encoding/MessageDigestPasswordEncoder.java

@@ -51,7 +51,10 @@ import org.springframework.util.Assert;
  * @author Ray Krueger
  * @author Luke Taylor
  * @since 1.0.1
+ * @deprecated This is deprecated and marked for deletion. Replace with an implementation
+ * of {@link org.springframework.security.crypto.password.MessageDigestPasswordEncoder}
  */
+@Deprecated
 public class MessageDigestPasswordEncoder extends BaseDigestPasswordEncoder {
 
 	private final String algorithm;

+ 3 - 0
core/src/main/java/org/springframework/security/authentication/encoding/PlaintextPasswordEncoder.java

@@ -29,7 +29,10 @@ import java.util.Locale;
  *
  * @author colin sampaleanu
  * @author Ben Alex
+ * @deprecated This class will be removed in Spring Security 5. For passivity switch to
+ * {@link org.springframework.security.crypto.password.NoOpPasswordEncoder}.
  */
+@Deprecated
 public class PlaintextPasswordEncoder extends BasePasswordEncoder {
 	// ~ Instance fields
 	// ================================================================================================

+ 4 - 0
core/src/main/java/org/springframework/security/authentication/encoding/ShaPasswordEncoder.java

@@ -40,7 +40,11 @@ package org.springframework.security.authentication.encoding;
  * @author Ray Krueger
  * @author colin sampaleanu
  * @author Ben Alex
+ * @deprecated This is deprecated and marked for deletion. Replace with an implementation
+ * of {@link org.springframework.security.crypto.password.MessageDigestPasswordEncoder}
+ * with an algorithm "SHA-$strength" (i.e. "SHA-1" or "SHA-256").
  */
+@Deprecated
 public class ShaPasswordEncoder extends MessageDigestPasswordEncoder {
 
 	/**

+ 79 - 0
crypto/src/main/java/org/springframework/security/crypto/factory/PasswordEncoderFactories.java

@@ -0,0 +1,79 @@
+/*
+ * Copyright 2002-2017 the original author or authors.
+ *
+ * Licensed 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.springframework.security.crypto.factory;
+
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
+import org.springframework.security.crypto.password.LdapShaPasswordEncoder;
+import org.springframework.security.crypto.password.Md4PasswordEncoder;
+import org.springframework.security.crypto.password.MessageDigestPasswordEncoder;
+import org.springframework.security.crypto.password.NoOpPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder;
+import org.springframework.security.crypto.password.StandardPasswordEncoder;
+import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Used for creating {@link PasswordEncoder} instances
+ * @author Rob Winch
+ * @since 4.2.6
+ */
+public class PasswordEncoderFactories {
+
+	/**
+	 * Creates a {@link DelegatingPasswordEncoder} with default mappings. Additional
+	 * mappings may be added and the encoding will be updated to conform with best
+	 * practices. However, due to the nature of {@link DelegatingPasswordEncoder} the
+	 * updates should not impact users. The mappings current are:
+	 *
+	 * <ul>
+	 * <li>bcrypt - {@link BCryptPasswordEncoder} (Also used for encoding)</li>
+	 * <li>ldap - {@link LdapShaPasswordEncoder}</li>
+	 * <li>MD4 - {@link Md4PasswordEncoder}</li>
+	 * <li>MD5 - {@code new MessageDigestPasswordEncoder("MD5")}</li>
+	 * <li>noop - {@link NoOpPasswordEncoder}</li>
+	 * <li>pbkdf2 - {@link Pbkdf2PasswordEncoder}</li>
+	 * <li>scrypt - {@link SCryptPasswordEncoder}</li>
+	 * <li>SHA-1 - {@code new MessageDigestPasswordEncoder("SHA-1")}</li>
+	 * <li>SHA-256 - {@code new MessageDigestPasswordEncoder("SHA-256")}</li>
+	 * <li>sha256 - {@link StandardPasswordEncoder}</li>
+	 * </ul>
+	 *
+	 * @return the {@link PasswordEncoder} to use
+	 */
+	public static PasswordEncoder createDelegatingPasswordEncoder() {
+		String encodingId = "bcrypt";
+		Map<String, PasswordEncoder> encoders = new HashMap<String, PasswordEncoder>();
+		encoders.put(encodingId, new BCryptPasswordEncoder());
+		encoders.put("ldap", new LdapShaPasswordEncoder());
+		encoders.put("MD4", new Md4PasswordEncoder());
+		encoders.put("MD5", new MessageDigestPasswordEncoder("MD5"));
+		encoders.put("noop", NoOpPasswordEncoder.getInstance());
+		encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
+		encoders.put("scrypt", new SCryptPasswordEncoder());
+		encoders.put("SHA-1", new MessageDigestPasswordEncoder("SHA-1"));
+		encoders.put("SHA-256", new MessageDigestPasswordEncoder("SHA-256"));
+		encoders.put("sha256", new StandardPasswordEncoder());
+
+		return new DelegatingPasswordEncoder(encodingId, encoders);
+	}
+
+	private PasswordEncoderFactories() {}
+}

+ 79 - 0
crypto/src/main/java/org/springframework/security/crypto/keygen/Base64StringKeyGenerator.java

@@ -0,0 +1,79 @@
+/*
+ * Copyright 2002-2017 the original author or authors.
+ *
+ * Licensed 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.springframework.security.crypto.keygen;
+
+import java.util.Base64;
+
+/**
+ * A StringKeyGenerator that generates base64-encoded String keys. Delegates to a
+ * {@link BytesKeyGenerator} for the actual key generation.
+ *
+ * @author Joe Grandja
+ * @author Rob Winch
+ * @since 4.2.6
+ */
+public class Base64StringKeyGenerator implements StringKeyGenerator {
+	private static final int DEFAULT_KEY_LENGTH = 32;
+	private final BytesKeyGenerator keyGenerator;
+	private final Base64.Encoder encoder;
+
+	/**
+	 * Creates an instance with keyLength of 32 bytes and standard Base64 encoding.
+	 */
+	public Base64StringKeyGenerator() {
+		this(DEFAULT_KEY_LENGTH);
+	}
+
+	/**
+	 * Creates an instance with the provided key length in bytes and standard Base64
+	 * encoding.
+	 * @param keyLength the key length in bytes
+	 */
+	public Base64StringKeyGenerator(int keyLength) {
+		this(Base64.getEncoder(), keyLength);
+	}
+
+	/**
+	 * Creates an instance with keyLength of 32 bytes and the provided encoder.
+	 * @param encoder the encoder to use
+	 */
+	public Base64StringKeyGenerator(Base64.Encoder encoder) {
+		this(encoder, DEFAULT_KEY_LENGTH);
+	}
+
+	/**
+	 * Creates an instance with the provided key length and encoder.
+	 * @param encoder the encoder to use
+	 * @param keyLength the key length to use
+	 */
+	public Base64StringKeyGenerator(Base64.Encoder encoder, int keyLength) {
+		if(encoder == null) {
+			throw new IllegalArgumentException("encode cannot be null");
+		}
+		if(keyLength < DEFAULT_KEY_LENGTH) {
+			throw new IllegalArgumentException("keyLength must be greater than or equal to" + DEFAULT_KEY_LENGTH);
+		}
+		this.encoder = encoder;
+		this.keyGenerator = KeyGenerators.secureRandom(keyLength);
+	}
+
+	@Override
+	public String generateKey() {
+		byte[] key = this.keyGenerator.generateKey();
+		byte[] base64EncodedKey = this.encoder.encode(key);
+		return new String(base64EncodedKey);
+	}
+}

+ 241 - 0
crypto/src/main/java/org/springframework/security/crypto/password/DelegatingPasswordEncoder.java

@@ -0,0 +1,241 @@
+/*
+ * Copyright 2002-2017 the original author or authors.
+ *
+ * Licensed 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.springframework.security.crypto.password;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A password encoder that delegates to another PasswordEncoder based upon a prefixed
+ * identifier.
+ *
+ * <h2>Constructing an instance</h2>
+ *
+ * You can easily construct an instance using
+ * {@link org.springframework.security.crypto.factory.PasswordEncoderFactories}.
+ * Alternatively, you may create your own custom instance. For example:
+ *
+ * <pre>
+ * String idForEncode = "bcrypt";
+ * Map<String,PasswordEncoder> encoders = new HashMap<>();
+ * encoders.put(idForEncode, new BCryptPasswordEncoder());
+ * encoders.put("noop", NoOpPasswordEncoder.getInstance());
+ * encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
+ * encoders.put("scrypt", new SCryptPasswordEncoder());
+ * encoders.put("sha256", new StandardPasswordEncoder());
+ *
+ * PasswordEncoder passwordEncoder = new DelegatingPasswordEncoder(idForEncode, encoders);
+ * </pre>
+ *
+ *
+ * <h2>Password Storage Format</h2>
+ *
+ * The general format for a password is:
+ *
+ * <pre>
+ * {id}encodedPassword
+ * </pre>
+ *
+ * Such that "id" is an identifier used to look up which {@link PasswordEncoder} should
+ * be used and "encodedPassword" is the original encoded password for the selected
+ * {@link PasswordEncoder}. The "id" must be at the beginning of the password, start with
+ * "{" and end with "}". If the "id" cannot be found, the "id" will be null.
+ *
+ * For example, the following might be a list of passwords encoded using different "id".
+ * All of the original passwords are "password".
+ *
+ * <pre>
+ * {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
+ * {noop}password
+ * {pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc
+ * {scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=
+ * {sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0
+ * </pre>
+ *
+ * For the DelegatingPasswordEncoder that we constructed above:
+ *
+ * <ol>
+ * <li>The first password would have a {@code PasswordEncoder} id of "bcrypt" and
+ * encodedPassword of "$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG".
+ * When matching it would delegate to
+ * {@link org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder}</li>
+ * <li>The second password would have a {@code PasswordEncoder} id of "noop" and
+ * encodedPassword of "password". When matching it would delegate to
+ * {@link NoOpPasswordEncoder}</li>
+ * <li>The third password would have a {@code PasswordEncoder} id of "pbkdf2" and
+ * encodedPassword of
+ * "5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc".
+ * When matching it would delegate to {@link Pbkdf2PasswordEncoder}</li>
+ * <li>The fourth password would have a {@code PasswordEncoder} id of "scrypt" and
+ * encodedPassword of
+ * "$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc="
+ * When matching it would delegate to
+ * {@link org.springframework.security.crypto.scrypt.SCryptPasswordEncoder}</li>
+ * <li>The final password would have a {@code PasswordEncoder} id of "sha256" and
+ * encodedPassword of
+ * "97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0".
+ * When matching it would delegate to {@link StandardPasswordEncoder}</li>
+ * </ol>
+ *
+ * <h2>Password Encoding</h2>
+ *
+ * The {@code idForEncode} passed into the constructor determines which
+ * {@link PasswordEncoder} will be used for encoding passwords. In the
+ * {@code DelegatingPasswordEncoder} we constructed above, that means that the result of
+ * encoding "password" would be delegated to {@code BCryptPasswordEncoder} and be prefixed
+ * with "{bcrypt}". The end result would look like:
+ *
+ * <pre>
+ * {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
+ * </pre>
+ *
+ * <h2>Password Matching</h2>
+ *
+ * Matching is done based upon the "id" and the mapping of the "id" to the
+ * {@link PasswordEncoder} provided in the constructor. Our example in "Password Storage
+ * Format" provides a working example of how this is done.
+ *
+ * By default the result of invoking {@link #matches(CharSequence, String)} with a
+ * password with an "id" that is not mapped (including a null id) will result in an
+ * {@link IllegalArgumentException}. This behavior can be customized using
+ * {@link #setDefaultPasswordEncoderForMatches(PasswordEncoder)}.
+ *
+ * @see org.springframework.security.crypto.factory.PasswordEncoderFactories
+ *
+ * @author Rob Winch
+ * @author Michael Simons
+ * @since 4.2.6
+ */
+public class DelegatingPasswordEncoder implements PasswordEncoder {
+	private static final String PREFIX = "{";
+	private static final String SUFFIX = "}";
+	private final String idForEncode;
+	private final PasswordEncoder passwordEncoderForEncode;
+	private final Map<String, PasswordEncoder> idToPasswordEncoder;
+	private PasswordEncoder defaultPasswordEncoderForMatches = new UnmappedIdPasswordEncoder();
+
+	/**
+	 * Creates a new instance
+	 * @param idForEncode the id used to lookup which {@link PasswordEncoder} should be
+	 * used for {@link #encode(CharSequence)}
+	 * @param idToPasswordEncoder a Map of id to {@link PasswordEncoder} used to determine
+	 * which {@link PasswordEncoder} should be used for {@link #matches(CharSequence, String)}
+	 */
+	public DelegatingPasswordEncoder(String idForEncode,
+		Map<String, PasswordEncoder> idToPasswordEncoder) {
+		if(idForEncode == null) {
+			throw new IllegalArgumentException("idForEncode cannot be null");
+		}
+		if(!idToPasswordEncoder.containsKey(idForEncode)) {
+			throw new IllegalArgumentException("idForEncode " + idForEncode + "is not found in idToPasswordEncoder " + idToPasswordEncoder);
+		}
+		for(String id : idToPasswordEncoder.keySet()) {
+			if(id == null) {
+				continue;
+			}
+			if(id.contains(PREFIX)) {
+				throw new IllegalArgumentException("id " + id + " cannot contain " + PREFIX);
+			}
+			if(id.contains(SUFFIX)) {
+				throw new IllegalArgumentException("id " + id + " cannot contain " + SUFFIX);
+			}
+		}
+		this.idForEncode = idForEncode;
+		this.passwordEncoderForEncode = idToPasswordEncoder.get(idForEncode);
+		this.idToPasswordEncoder = new HashMap<String, PasswordEncoder>(idToPasswordEncoder);
+	}
+
+	/**
+	 * Sets the {@link PasswordEncoder} to delegate to for
+	 * {@link #matches(CharSequence, String)} if the id is not mapped to a
+	 * {@link PasswordEncoder}.
+	 *
+	 * <p>
+	   The encodedPassword provided will be the full password
+	 * passed in including the {"id"} portion.* For example, if the password of
+	 * "{notmapped}foobar" was used, the "id" would be "notmapped" and the encodedPassword
+	 * passed into the {@link PasswordEncoder} would be "{notmapped}foobar".
+	 * </p>
+	 * @param defaultPasswordEncoderForMatches the encoder to use. The default is to
+	 * throw an {@link IllegalArgumentException}
+	 */
+	public void setDefaultPasswordEncoderForMatches(
+		PasswordEncoder defaultPasswordEncoderForMatches) {
+		if(defaultPasswordEncoderForMatches == null) {
+			throw new IllegalArgumentException("defaultPasswordEncoderForMatches cannot be null");
+		}
+		this.defaultPasswordEncoderForMatches = defaultPasswordEncoderForMatches;
+	}
+
+	@Override
+	public String encode(CharSequence rawPassword) {
+		return PREFIX + this.idForEncode + SUFFIX + this.passwordEncoderForEncode.encode(rawPassword);
+	}
+
+	@Override
+	public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {
+		if(rawPassword == null && prefixEncodedPassword == null) {
+			return true;
+		}
+		String id = extractId(prefixEncodedPassword);
+		PasswordEncoder delegate = this.idToPasswordEncoder.get(id);
+		if(delegate == null) {
+			return this.defaultPasswordEncoderForMatches
+				.matches(rawPassword, prefixEncodedPassword);
+		}
+		String encodedPassword = extractEncodedPassword(prefixEncodedPassword);
+		return delegate.matches(rawPassword, encodedPassword);
+	}
+
+	private String extractId(String prefixEncodedPassword) {
+		if (prefixEncodedPassword == null) {
+			return null;
+		}
+		int start = prefixEncodedPassword.indexOf(PREFIX);
+		if(start != 0) {
+			return null;
+		}
+		int end = prefixEncodedPassword.indexOf(SUFFIX, start);
+		if(end < 0) {
+			return null;
+		}
+		return prefixEncodedPassword.substring(start + 1, end);
+	}
+
+	private String extractEncodedPassword(String prefixEncodedPassword) {
+		int start = prefixEncodedPassword.indexOf(SUFFIX);
+		return prefixEncodedPassword.substring(start + 1);
+	}
+
+	/**
+	 * Default {@link PasswordEncoder} that throws an exception that a id could
+	 */
+	private class UnmappedIdPasswordEncoder implements PasswordEncoder {
+
+		@Override
+		public String encode(CharSequence rawPassword) {
+			throw new UnsupportedOperationException("encode is not supported");
+		}
+
+		@Override
+		public boolean matches(CharSequence rawPassword,
+			String prefixEncodedPassword) {
+			String id = extractId(prefixEncodedPassword);
+			throw new IllegalArgumentException("There is no PasswordEncoder mapped for the id \"" + id + "\"");
+		}
+	}
+}

+ 9 - 2
crypto/src/main/java/org/springframework/security/crypto/password/Digester.java

@@ -31,7 +31,7 @@ final class Digester {
 
 	private final String algorithm;
 
-	private final int iterations;
+	private int iterations;
 
 	/**
 	 * Create a new Digester.
@@ -42,7 +42,7 @@ final class Digester {
 		// eagerly validate the algorithm
 		createDigest(algorithm);
 		this.algorithm = algorithm;
-		this.iterations = iterations;
+		setIterations(iterations);
 	}
 
 	public byte[] digest(byte[] value) {
@@ -53,6 +53,13 @@ final class Digester {
 		return value;
 	}
 
+	final void setIterations(int iterations) {
+		if(iterations <= 0) {
+			throw new IllegalArgumentException("Iterations value must be greater than zero");
+		}
+		this.iterations = iterations;
+	}
+
 	private static MessageDigest createDigest(String algorithm) {
 		try {
 			return MessageDigest.getInstance(algorithm);

+ 211 - 0
crypto/src/main/java/org/springframework/security/crypto/password/LdapShaPasswordEncoder.java

@@ -0,0 +1,211 @@
+/*
+ * Copyright 2002-2017 the original author or authors.
+ *
+ * Licensed 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.springframework.security.crypto.password;
+
+import org.springframework.security.crypto.codec.Utf8;
+import org.springframework.security.crypto.keygen.BytesKeyGenerator;
+import org.springframework.security.crypto.keygen.KeyGenerators;
+
+import java.security.MessageDigest;
+import java.util.Base64;
+
+/**
+ * This {@link PasswordEncoder} is provided for legacy purposes only and is not considered
+ * secure.
+ *
+ * A version of {@link PasswordEncoder} which supports Ldap SHA and SSHA (salted-SHA)
+ * encodings. The values are base-64 encoded and have the label "{SHA}" (or "{SSHA}")
+ * prepended to the encoded hash. These can be made lower-case in the encoded password, if
+ * required, by setting the <tt>forceLowerCasePrefix</tt> property to true.
+ *
+ * Also supports plain text passwords, so can safely be used in cases when both encoded
+ * and non-encoded passwords are in use or when a null implementation is required.
+ *
+ * @author Luke Taylor
+ * @since 4.2.6
+ * @deprecated Digest based password encoding is not considered secure. Instead use an
+ * adaptive one way funciton like BCryptPasswordEncoder, Pbkdf2PasswordEncoder, or
+ * SCryptPasswordEncoder. Even better use {@link DelegatingPasswordEncoder} which supports
+ * password upgrades. There are no plans to remove this support. It is deprecated to indicate
+ * that this is a legacy implementation and using it is considered insecure.
+ */
+@Deprecated
+public class LdapShaPasswordEncoder implements PasswordEncoder {
+	// ~ Static fields/initializers
+	// =====================================================================================
+
+	/** The number of bytes in a SHA hash */
+	private static final int SHA_LENGTH = 20;
+	private static final String SSHA_PREFIX = "{SSHA}";
+	private static final String SSHA_PREFIX_LC = SSHA_PREFIX.toLowerCase();
+	private static final String SHA_PREFIX = "{SHA}";
+	private static final String SHA_PREFIX_LC = SHA_PREFIX.toLowerCase();
+
+	// ~ Instance fields
+	// ================================================================================================
+	private BytesKeyGenerator saltGenerator;
+
+	private boolean forceLowerCasePrefix;
+
+	// ~ Constructors
+	// ===================================================================================================
+
+	public LdapShaPasswordEncoder() {
+		this(KeyGenerators.secureRandom());
+	}
+
+	public LdapShaPasswordEncoder(BytesKeyGenerator saltGenerator) {
+		if(saltGenerator == null) {
+			throw new IllegalArgumentException("saltGenerator cannot be null");
+		}
+		this.saltGenerator = saltGenerator;
+	}
+
+	// ~ Methods
+	// ========================================================================================================
+
+	private byte[] combineHashAndSalt(byte[] hash, byte[] salt) {
+		if (salt == null) {
+			return hash;
+		}
+
+		byte[] hashAndSalt = new byte[hash.length + salt.length];
+		System.arraycopy(hash, 0, hashAndSalt, 0, hash.length);
+		System.arraycopy(salt, 0, hashAndSalt, hash.length, salt.length);
+
+		return hashAndSalt;
+	}
+
+	/**
+	 * Calculates the hash of password (and salt bytes, if supplied) and returns a base64
+	 * encoded concatenation of the hash and salt, prefixed with {SHA} (or {SSHA} if salt
+	 * was used).
+	 *
+	 * @param rawPass the password to be encoded.
+	 *
+	 * @return the encoded password in the specified format
+	 *
+	 */
+	public String encode(CharSequence rawPass) {
+		byte[] salt = this.saltGenerator.generateKey();
+		return encode(rawPass, salt);
+	}
+
+
+	private String encode(CharSequence rawPassword, byte[] salt) {
+		MessageDigest sha;
+
+		try {
+			sha = MessageDigest.getInstance("SHA");
+			sha.update(Utf8.encode(rawPassword));
+		}
+		catch (java.security.NoSuchAlgorithmException e) {
+			throw new IllegalStateException("No SHA implementation available!");
+		}
+
+		if (salt != null) {
+			sha.update(salt);
+		}
+
+		byte[] hash = combineHashAndSalt(sha.digest(), (byte[]) salt);
+
+		String prefix;
+
+		if (salt == null || salt.length == 0) {
+			prefix = forceLowerCasePrefix ? SHA_PREFIX_LC : SHA_PREFIX;
+		}
+		else {
+			prefix = forceLowerCasePrefix ? SSHA_PREFIX_LC : SSHA_PREFIX;
+		}
+
+		return prefix + Utf8.decode(Base64.getEncoder().encode(hash));
+	}
+
+	private byte[] extractSalt(String encPass) {
+		String encPassNoLabel = encPass.substring(6);
+
+		byte[] hashAndSalt = Base64.getDecoder().decode(encPassNoLabel.getBytes());
+		int saltLength = hashAndSalt.length - SHA_LENGTH;
+		byte[] salt = new byte[saltLength];
+		System.arraycopy(hashAndSalt, SHA_LENGTH, salt, 0, saltLength);
+
+		return salt;
+	}
+
+	/**
+	 * Checks the validity of an unencoded password against an encoded one in the form
+	 * "{SSHA}sQuQF8vj8Eg2Y1hPdh3bkQhCKQBgjhQI".
+	 *
+	 * @param rawPassword unencoded password to be verified.
+	 * @param encodedPassword the actual SSHA or SHA encoded password
+	 *
+	 * @return true if they match (independent of the case of the prefix).
+	 */
+	public boolean matches(CharSequence rawPassword, String encodedPassword) {
+		return matches(rawPassword == null ? null : rawPassword.toString(), encodedPassword);
+	}
+
+	private boolean matches(String rawPassword, String encodedPassword) {
+		String prefix = extractPrefix(encodedPassword);
+
+		if (prefix == null) {
+			return PasswordEncoderUtils.equals(encodedPassword, rawPassword);
+		}
+
+		byte[] salt;
+		if (prefix.equals(SSHA_PREFIX) || prefix.equals(SSHA_PREFIX_LC)) {
+			salt = extractSalt(encodedPassword);
+		}
+		else if (!prefix.equals(SHA_PREFIX) && !prefix.equals(SHA_PREFIX_LC)) {
+			throw new IllegalArgumentException("Unsupported password prefix '" + prefix
+					+ "'");
+		}
+		else {
+			// Standard SHA
+			salt = null;
+		}
+
+		int startOfHash = prefix.length();
+
+		String encodedRawPass = encode(rawPassword, salt).substring(startOfHash);
+
+		return PasswordEncoderUtils
+				.equals(encodedRawPass, encodedPassword.substring(startOfHash));
+	}
+
+	/**
+	 * Returns the hash prefix or null if there isn't one.
+	 */
+	private String extractPrefix(String encPass) {
+		if (!encPass.startsWith("{")) {
+			return null;
+		}
+
+		int secondBrace = encPass.lastIndexOf('}');
+
+		if (secondBrace < 0) {
+			throw new IllegalArgumentException(
+					"Couldn't find closing brace for SHA prefix");
+		}
+
+		return encPass.substring(0, secondBrace + 1);
+	}
+
+	public void setForceLowerCasePrefix(boolean forceLowerCasePrefix) {
+		this.forceLowerCasePrefix = forceLowerCasePrefix;
+	}
+}

+ 182 - 0
crypto/src/main/java/org/springframework/security/crypto/password/Md4.java

@@ -0,0 +1,182 @@
+/*
+ * Copyright 2002-2017 the original author or authors.
+ *
+ * Licensed 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.springframework.security.crypto.password;
+
+/**
+ * Implementation of the MD4 message digest derived from the RSA Data Security, Inc, MD4
+ * Message-Digest Algorithm.
+ *
+ * @author Alan Stewart
+ */
+class Md4 {
+	private static final int BLOCK_SIZE = 64;
+	private static final int HASH_SIZE = 16;
+	private final byte[] buffer = new byte[BLOCK_SIZE];
+	private int bufferOffset;
+	private long byteCount;
+	private final int[] state = new int[4];
+	private final int[] tmp = new int[16];
+
+	Md4() {
+		reset();
+	}
+
+	public void reset() {
+		bufferOffset = 0;
+		byteCount = 0;
+		state[0] = 0x67452301;
+		state[1] = 0xEFCDAB89;
+		state[2] = 0x98BADCFE;
+		state[3] = 0x10325476;
+	}
+
+	public byte[] digest() {
+		byte[] resBuf = new byte[HASH_SIZE];
+		digest(resBuf, 0, HASH_SIZE);
+		return resBuf;
+	}
+
+	private void digest(byte[] buffer, int off) {
+		for (int i = 0; i < 4; i++) {
+			for (int j = 0; j < 4; j++) {
+				buffer[off + (i * 4 + j)] = (byte) (state[i] >>> (8 * j));
+			}
+		}
+	}
+
+	private void digest(byte[] buffer, int offset, int len) {
+		this.buffer[this.bufferOffset++] = (byte) 0x80;
+		int lenOfBitLen = 8;
+		int C = BLOCK_SIZE - lenOfBitLen;
+		if (this.bufferOffset > C) {
+			while (this.bufferOffset < BLOCK_SIZE) {
+				this.buffer[this.bufferOffset++] = (byte) 0x00;
+			}
+			update(this.buffer, 0);
+			this.bufferOffset = 0;
+		}
+
+		while (this.bufferOffset < C) {
+			this.buffer[this.bufferOffset++] = (byte) 0x00;
+		}
+
+		long bitCount = byteCount * 8;
+		for (int i = 0; i < 64; i += 8) {
+			this.buffer[this.bufferOffset++] = (byte) (bitCount >>> (i));
+		}
+
+		update(this.buffer, 0);
+		digest(buffer, offset);
+	}
+
+	public void update(byte[] input, int offset, int length) {
+		byteCount += length;
+		int todo;
+		while (length >= (todo = BLOCK_SIZE - this.bufferOffset)) {
+			System.arraycopy(input, offset, this.buffer, this.bufferOffset, todo);
+			update(this.buffer, 0);
+			length -= todo;
+			offset += todo;
+			this.bufferOffset = 0;
+		}
+
+		System.arraycopy(input, offset, this.buffer, this.bufferOffset, length);
+		bufferOffset += length;
+	}
+
+	private void update(byte[] block, int offset) {
+		for (int i = 0; i < 16; i++) {
+			tmp[i] = (block[offset++] & 0xFF) | (block[offset++] & 0xFF) << 8
+					| (block[offset++] & 0xFF) << 16 | (block[offset++] & 0xFF) << 24;
+		}
+
+		int A = state[0];
+		int B = state[1];
+		int C = state[2];
+		int D = state[3];
+
+		A = FF(A, B, C, D, tmp[0], 3);
+		D = FF(D, A, B, C, tmp[1], 7);
+		C = FF(C, D, A, B, tmp[2], 11);
+		B = FF(B, C, D, A, tmp[3], 19);
+		A = FF(A, B, C, D, tmp[4], 3);
+		D = FF(D, A, B, C, tmp[5], 7);
+		C = FF(C, D, A, B, tmp[6], 11);
+		B = FF(B, C, D, A, tmp[7], 19);
+		A = FF(A, B, C, D, tmp[8], 3);
+		D = FF(D, A, B, C, tmp[9], 7);
+		C = FF(C, D, A, B, tmp[10], 11);
+		B = FF(B, C, D, A, tmp[11], 19);
+		A = FF(A, B, C, D, tmp[12], 3);
+		D = FF(D, A, B, C, tmp[13], 7);
+		C = FF(C, D, A, B, tmp[14], 11);
+		B = FF(B, C, D, A, tmp[15], 19);
+
+		A = GG(A, B, C, D, tmp[0], 3);
+		D = GG(D, A, B, C, tmp[4], 5);
+		C = GG(C, D, A, B, tmp[8], 9);
+		B = GG(B, C, D, A, tmp[12], 13);
+		A = GG(A, B, C, D, tmp[1], 3);
+		D = GG(D, A, B, C, tmp[5], 5);
+		C = GG(C, D, A, B, tmp[9], 9);
+		B = GG(B, C, D, A, tmp[13], 13);
+		A = GG(A, B, C, D, tmp[2], 3);
+		D = GG(D, A, B, C, tmp[6], 5);
+		C = GG(C, D, A, B, tmp[10], 9);
+		B = GG(B, C, D, A, tmp[14], 13);
+		A = GG(A, B, C, D, tmp[3], 3);
+		D = GG(D, A, B, C, tmp[7], 5);
+		C = GG(C, D, A, B, tmp[11], 9);
+		B = GG(B, C, D, A, tmp[15], 13);
+
+		A = HH(A, B, C, D, tmp[0], 3);
+		D = HH(D, A, B, C, tmp[8], 9);
+		C = HH(C, D, A, B, tmp[4], 11);
+		B = HH(B, C, D, A, tmp[12], 15);
+		A = HH(A, B, C, D, tmp[2], 3);
+		D = HH(D, A, B, C, tmp[10], 9);
+		C = HH(C, D, A, B, tmp[6], 11);
+		B = HH(B, C, D, A, tmp[14], 15);
+		A = HH(A, B, C, D, tmp[1], 3);
+		D = HH(D, A, B, C, tmp[9], 9);
+		C = HH(C, D, A, B, tmp[5], 11);
+		B = HH(B, C, D, A, tmp[13], 15);
+		A = HH(A, B, C, D, tmp[3], 3);
+		D = HH(D, A, B, C, tmp[11], 9);
+		C = HH(C, D, A, B, tmp[7], 11);
+		B = HH(B, C, D, A, tmp[15], 15);
+
+		state[0] += A;
+		state[1] += B;
+		state[2] += C;
+		state[3] += D;
+	}
+
+	private int FF(int a, int b, int c, int d, int x, int s) {
+		int t = a + ((b & c) | (~b & d)) + x;
+		return t << s | t >>> (32 - s);
+	}
+
+	private int GG(int a, int b, int c, int d, int x, int s) {
+		int t = a + ((b & (c | d)) | (c & d)) + x + 0x5A827999;
+		return t << s | t >>> (32 - s);
+	}
+
+	private int HH(int a, int b, int c, int d, int x, int s) {
+		int t = a + (b ^ c ^ d) + x + 0x6ED9EBA1;
+		return t << s | t >>> (32 - s);
+	}
+}

+ 155 - 0
crypto/src/main/java/org/springframework/security/crypto/password/Md4PasswordEncoder.java

@@ -0,0 +1,155 @@
+/*
+ * Copyright 2002-2017 the original author or authors.
+ *
+ * Licensed 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.springframework.security.crypto.password;
+
+import org.springframework.security.crypto.codec.Hex;
+import org.springframework.security.crypto.codec.Utf8;
+import org.springframework.security.crypto.keygen.Base64StringKeyGenerator;
+import org.springframework.security.crypto.keygen.StringKeyGenerator;
+
+import java.util.Base64;
+
+/**
+ * This {@link PasswordEncoder} is provided for legacy purposes only and is not considered secure.
+ *
+ * Encodes passwords using MD4. The general format of the password is:
+ *
+ * <pre>
+ * s = salt == null ? "" : "{" + salt + "}"
+ * s + md4(password + s)
+ * </pre>
+ *
+ * Such that "salt" is the salt, md4 is the digest method, and password is the actual
+ * password. For example with a password of "password", and a salt of
+ * "thisissalt":
+ *
+ * <pre>
+ * String s = salt == null ? "" : "{" + salt + "}";
+ * s + md4(password + s)
+ * "{thisissalt}" + md4(password + "{thisissalt}")
+ * "{thisissalt}6cc7924dad12ade79dfb99e424f25260"
+ * </pre>
+ *
+ * If the salt does not exist, then omit "{salt}" like this:
+ *
+ * <pre>
+ * md4(password)
+ * </pre>
+ *
+ * If the salt is an empty String, then only use "{}" like this:
+ *
+ * <pre>
+ * "{}" + md4(password + "{}")
+ * </pre>
+ *
+ * The format is intended to work with the Md4PasswordEncoder that was found in the
+ * Spring Security core module. However, the passwords will need to be migrated to include
+ * any salt with the password since this API provides Salt internally vs making it the
+ * responsibility of the user. To migrate passwords from the SaltSource use the following:
+ *
+ * <pre>
+ * String salt = saltSource.getSalt(user);
+ * String s = salt == null ? null : "{" + salt + "}";
+ * String migratedPassword = s + user.getPassword();
+ * </pre>
+ *
+ * @author Ray Krueger
+ * @author Luke Taylor
+ * @author Rob winch
+ * @since 5.1
+ * @deprecated Digest based password encoding is not considered secure. Instead use an
+ * adaptive one way funciton like BCryptPasswordEncoder, Pbkdf2PasswordEncoder, or
+ * SCryptPasswordEncoder. Even better use {@link DelegatingPasswordEncoder} which supports
+ * password upgrades. There are no plans to remove this support. It is deprecated to indicate
+ * that this is a legacy implementation and using it is considered insecure.
+ */
+@Deprecated
+public class Md4PasswordEncoder implements PasswordEncoder {
+	private static final String PREFIX = "{";
+	private static final String SUFFIX = "}";
+	private StringKeyGenerator saltGenerator = new Base64StringKeyGenerator();
+	private boolean encodeHashAsBase64;
+
+	private Digester digester;
+
+
+	public void setEncodeHashAsBase64(boolean encodeHashAsBase64) {
+		this.encodeHashAsBase64 = encodeHashAsBase64;
+	}
+
+	/**
+	 * Encodes the rawPass using a MessageDigest. If a salt is specified it will be merged
+	 * with the password before encoding.
+	 *
+	 * @param rawPassword The plain text password
+	 * @return Hex string of password digest (or base64 encoded string if
+	 * encodeHashAsBase64 is enabled.
+	 */
+	public String encode(CharSequence rawPassword) {
+		String salt = PREFIX + this.saltGenerator.generateKey() + SUFFIX;
+		return digest(salt, rawPassword);
+	}
+
+	private String digest(String salt, CharSequence rawPassword) {
+		if(rawPassword == null) {
+			rawPassword = "";
+		}
+		String saltedPassword = rawPassword + salt;
+		byte[] saltedPasswordBytes = Utf8.encode(saltedPassword);
+
+		Md4 md4 = new Md4();
+		md4.update(saltedPasswordBytes, 0, saltedPasswordBytes.length);
+
+		byte[] digest = md4.digest();
+		String encoded = encode(digest);
+		return salt + encoded;
+	}
+
+	private String encode(byte[] digest) {
+		if (this.encodeHashAsBase64) {
+			return Utf8.decode(Base64.getEncoder().encode(digest));
+		}
+		else {
+			return new String(Hex.encode(digest));
+		}
+	}
+
+	/**
+	 * Takes a previously encoded password and compares it with a rawpassword after mixing
+	 * in the salt and encoding that value
+	 *
+	 * @param rawPassword plain text password
+	 * @param encodedPassword previously encoded password
+	 * @return true or false
+	 */
+	public boolean matches(CharSequence rawPassword, String encodedPassword) {
+		String salt = extractSalt(encodedPassword);
+		String rawPasswordEncoded = digest(salt, rawPassword);
+		return PasswordEncoderUtils.equals(encodedPassword.toString(), rawPasswordEncoded);
+	}
+
+	private String extractSalt(String prefixEncodedPassword) {
+		int start = prefixEncodedPassword.indexOf(PREFIX);
+		if(start != 0) {
+			return "";
+		}
+		int end = prefixEncodedPassword.indexOf(SUFFIX, start);
+		if(end < 0) {
+			return "";
+		}
+		return prefixEncodedPassword.substring(start, end + 1);
+	}
+}

+ 174 - 0
crypto/src/main/java/org/springframework/security/crypto/password/MessageDigestPasswordEncoder.java

@@ -0,0 +1,174 @@
+/*
+ * Copyright 2002-2017 the original author or authors.
+ *
+ * Licensed 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.springframework.security.crypto.password;
+
+import org.springframework.security.crypto.codec.Hex;
+import org.springframework.security.crypto.codec.Utf8;
+import org.springframework.security.crypto.keygen.Base64StringKeyGenerator;
+import org.springframework.security.crypto.keygen.StringKeyGenerator;
+
+import java.security.MessageDigest;
+import java.util.Base64;
+
+/**
+ * This {@link PasswordEncoder} is provided for legacy purposes only and is not considered secure.
+ *
+ * Encodes passwords using the passed in {@link MessageDigest}.
+ *
+ * The general format of the password is:
+ *
+ * <pre>
+ * s = salt == null ? "" : "{" + salt + "}"
+ * s + digest(password + s)
+ * </pre>
+ *
+ * Such that "salt" is the salt, digest is the digest method, and password is the actual
+ * password. For example when using MD5, a password of "password", and a salt of
+ * "thisissalt":
+ *
+ * <pre>
+ * String s = salt == null ? "" : "{" + salt + "}";
+ * s + md5(password + s)
+ * "{thisissalt}" + md5(password + "{thisissalt}")
+ * "{thisissalt}2a4e7104c2780098f50ed5a84bb2323d"
+ * </pre>
+ *
+ * If the salt does not exist, then omit "{salt}" like this:
+ *
+ * <pre>
+ * digest(password)
+ * </pre>
+ *
+ * If the salt is an empty String, then only use "{}" like this:
+ *
+ * <pre>
+ * "{}" + digest(password + "{}")
+ * </pre>
+ *
+ * The format is intended to work with the DigestPasswordEncoder that was found in the
+ * Spring Security core module. However, the passwords will need to be migrated to include
+ * any salt with the password since this API provides Salt internally vs making it the
+ * responsibility of the user. To migrate passwords from the SaltSource use the following:
+ *
+ * <pre>
+ * String salt = saltSource.getSalt(user);
+ * String s = salt == null ? null : "{" + salt + "}";
+ * String migratedPassword = s + user.getPassword();
+ * </pre>
+ *
+ * @author Ray Krueger
+ * @author Luke Taylor
+ * @author Rob Winch
+ * @since 4.2.6
+ * @deprecated Digest based password encoding is not considered secure. Instead use an
+ * adaptive one way funciton like BCryptPasswordEncoder, Pbkdf2PasswordEncoder, or
+ * SCryptPasswordEncoder. Even better use {@link DelegatingPasswordEncoder} which supports
+ * password upgrades. There are no plans to remove this support. It is deprecated to indicate
+ * that this is a legacy implementation and using it is considered insecure.
+ */
+@Deprecated
+public class MessageDigestPasswordEncoder implements PasswordEncoder {
+	private static final String PREFIX = "{";
+	private static final String SUFFIX = "}";
+	private StringKeyGenerator saltGenerator = new Base64StringKeyGenerator();
+	private boolean encodeHashAsBase64;
+
+	private Digester digester;
+
+	/**
+	 * The digest algorithm to use Supports the named
+	 * <a href="http://java.sun.com/j2se/1.4.2/docs/guide/security/CryptoSpec.html#AppA">
+	 * Message Digest Algorithms</a> in the Java environment.
+	 *
+	 * @param algorithm
+	 */
+	public MessageDigestPasswordEncoder(String algorithm) {
+		this.digester = new Digester(algorithm, 1);
+	}
+
+	public void setEncodeHashAsBase64(boolean encodeHashAsBase64) {
+		this.encodeHashAsBase64 = encodeHashAsBase64;
+	}
+
+	/**
+	 * Encodes the rawPass using a MessageDigest. If a salt is specified it will be merged
+	 * with the password before encoding.
+	 *
+	 * @param rawPassword The plain text password
+	 * @return Hex string of password digest (or base64 encoded string if
+	 * encodeHashAsBase64 is enabled.
+	 */
+	public String encode(CharSequence rawPassword) {
+		String salt = PREFIX + this.saltGenerator.generateKey() + SUFFIX;
+		return digest(salt, rawPassword);
+	}
+
+	private String digest(String salt, CharSequence rawPassword) {
+		String saltedPassword = rawPassword + salt;
+
+		byte[] digest = this.digester.digest(Utf8.encode(saltedPassword));
+		String encoded = encode(digest);
+		return salt + encoded;
+	}
+
+	private String encode(byte[] digest) {
+		if (this.encodeHashAsBase64) {
+			return Utf8.decode(Base64.getEncoder().encode(digest));
+		}
+		else {
+			return new String(Hex.encode(digest));
+		}
+	}
+
+	/**
+	 * Takes a previously encoded password and compares it with a rawpassword after mixing
+	 * in the salt and encoding that value
+	 *
+	 * @param rawPassword plain text password
+	 * @param encodedPassword previously encoded password
+	 * @return true or false
+	 */
+	public boolean matches(CharSequence rawPassword, String encodedPassword) {
+		String salt = extractSalt(encodedPassword);
+		String rawPasswordEncoded = digest(salt, rawPassword);
+		return PasswordEncoderUtils.equals(encodedPassword.toString(), rawPasswordEncoded);
+	}
+
+	/**
+	 * Sets the number of iterations for which the calculated hash value should be
+	 * "stretched". If this is greater than one, the initial digest is calculated, the
+	 * digest function will be called repeatedly on the result for the additional number
+	 * of iterations.
+	 *
+	 * @param iterations the number of iterations which will be executed on the hashed
+	 * password/salt value. Defaults to 1.
+	 */
+	public void setIterations(int iterations) {
+		this.digester.setIterations(iterations);
+	}
+
+	private String extractSalt(String prefixEncodedPassword) {
+		int start = prefixEncodedPassword.indexOf(PREFIX);
+		if(start != 0) {
+			return "";
+		}
+		int end = prefixEncodedPassword.indexOf(SUFFIX, start);
+		if(end < 0) {
+			return "";
+		}
+		return prefixEncodedPassword.substring(start, end + 1);
+	}
+}

+ 8 - 0
crypto/src/main/java/org/springframework/security/crypto/password/NoOpPasswordEncoder.java

@@ -16,11 +16,19 @@
 package org.springframework.security.crypto.password;
 
 /**
+ * This {@link PasswordEncoder} is provided for legacy and testing purposes only and is
+ * not considered secure.
+ *
  * A password encoder that does nothing. Useful for testing where working with plain text
  * passwords may be preferred.
  *
  * @author Keith Donald
+ * @deprecated This PasswordEncoder is not secure. Instead use an
+ * adaptive one way function like BCryptPasswordEncoder, Pbkdf2PasswordEncoder, or
+ * SCryptPasswordEncoder. Even better use {@link DelegatingPasswordEncoder} which supports
+ * password upgrades.
  */
+@Deprecated
 public final class NoOpPasswordEncoder implements PasswordEncoder {
 
 	public String encode(CharSequence rawPassword) {

+ 58 - 0
crypto/src/main/java/org/springframework/security/crypto/password/PasswordEncoderUtils.java

@@ -0,0 +1,58 @@
+/*
+ * Copyright 2002-2017 the original author or authors.
+ *
+ * Licensed 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.springframework.security.crypto.password;
+
+import org.springframework.security.crypto.codec.Utf8;
+
+/**
+ * Utility for constant time comparison to prevent against timing attacks.
+ *
+ * @author Rob Winch
+ */
+class PasswordEncoderUtils {
+
+	/**
+	 * Constant time comparison to prevent against timing attacks.
+	 * @param expected
+	 * @param actual
+	 * @return
+	 */
+	static boolean equals(String expected, String actual) {
+		byte[] expectedBytes = bytesUtf8(expected);
+		byte[] actualBytes = bytesUtf8(actual);
+		int expectedLength = expectedBytes == null ? -1 : expectedBytes.length;
+		int actualLength = actualBytes == null ? -1 : actualBytes.length;
+
+		int result = expectedLength == actualLength ? 0 : 1;
+		for (int i = 0; i < actualLength; i++) {
+			byte expectedByte = expectedLength <= 0 ? 0 : expectedBytes[i % expectedLength];
+			byte actualByte = actualBytes[i % actualLength];
+			result |= expectedByte ^ actualByte;
+		}
+		return result == 0;
+	}
+
+	private static byte[] bytesUtf8(String s) {
+		if (s == null) {
+			return null;
+		}
+
+		return Utf8.encode(s); // need to check if Utf8.encode() runs in constant time (probably not). This may leak length of string.
+	}
+
+	private PasswordEncoderUtils() {
+	}
+}

+ 67 - 5
crypto/src/main/java/org/springframework/security/crypto/password/Pbkdf2PasswordEncoder.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2016 the original author or authors.
+ * Copyright 2002-2017 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -16,6 +16,8 @@
 package org.springframework.security.crypto.password;
 
 import java.security.GeneralSecurityException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Base64;
 
 import javax.crypto.SecretKeyFactory;
 import javax.crypto.spec.PBEKeySpec;
@@ -41,7 +43,7 @@ import static org.springframework.security.crypto.util.EncodingUtils.subArray;
  * @since 4.1
  */
 public class Pbkdf2PasswordEncoder implements PasswordEncoder {
-	private static final String PBKDF2_ALGORITHM = "PBKDF2WithHmacSHA1";
+
 	private static final int DEFAULT_HASH_WIDTH = 256;
 	private static final int DEFAULT_ITERATIONS = 185000;
 
@@ -50,6 +52,8 @@ public class Pbkdf2PasswordEncoder implements PasswordEncoder {
 	private final byte[] secret;
 	private final int hashWidth;
 	private final int iterations;
+	private String algorithm = SecretKeyFactoryAlgorithm.PBKDF2WithHmacSHA1.name();
+	private boolean encodeHashAsBase64;
 
 	/**
 	 * Constructs a PBKDF2 password encoder with no additional secret value. There will be
@@ -86,16 +90,56 @@ public class Pbkdf2PasswordEncoder implements PasswordEncoder {
 		this.hashWidth = hashWidth;
 	}
 
+	/**
+	 * Sets the algorithm to use. See
+	 * <a href="http://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html#SecretKeyFactory">SecretKeyFactory Algorithms</a>
+	 * @param secretKeyFactoryAlgorithm the algorithm to use (i.e.
+	 * {@code SecretKeyFactoryAlgorithm.PBKDF2WithHmacSHA1},
+	 * {@code SecretKeyFactoryAlgorithm.PBKDF2WithHmacSHA256},
+	 * {@code SecretKeyFactoryAlgorithm.PBKDF2WithHmacSHA512})
+	 * @since 5.0
+	 */
+	public void setAlgorithm(SecretKeyFactoryAlgorithm secretKeyFactoryAlgorithm) {
+		if(secretKeyFactoryAlgorithm == null) {
+			throw new IllegalArgumentException("secretKeyFactoryAlgorithm cannot be null");
+		}
+		String algorithmName = secretKeyFactoryAlgorithm.name();
+		try {
+			SecretKeyFactory.getInstance(algorithmName);
+		}
+		catch (NoSuchAlgorithmException e) {
+			throw new IllegalArgumentException("Invalid algorithm '" + algorithmName + "'.", e);
+		}
+		this.algorithm = algorithmName;
+	}
+
+	/**
+	 * Sets if the resulting hash should be encoded as Base64. The default is false which
+	 * means it will be encoded in Hex.
+	 * @param encodeHashAsBase64 true if encode as Base64, false if should use Hex
+	 * (default)
+	 */
+	public void setEncodeHashAsBase64(boolean encodeHashAsBase64) {
+		this.encodeHashAsBase64 = encodeHashAsBase64;
+	}
+
 	@Override
 	public String encode(CharSequence rawPassword) {
 		byte[] salt = this.saltGenerator.generateKey();
 		byte[] encoded = encode(rawPassword, salt);
-		return String.valueOf(Hex.encode(encoded));
+		return encode(encoded);
+	}
+
+	private String encode(byte[] bytes) {
+		if(this.encodeHashAsBase64) {
+			return Base64.getEncoder().encodeToString(bytes);
+		}
+		return String.valueOf(Hex.encode(bytes));
 	}
 
 	@Override
 	public boolean matches(CharSequence rawPassword, String encodedPassword) {
-		byte[] digested = Hex.decode(encodedPassword);
+		byte[] digested = decode(encodedPassword);
 		byte[] salt = subArray(digested, 0, this.saltGenerator.getKeyLength());
 		return matches(digested, encode(rawPassword, salt));
 	}
@@ -115,15 +159,33 @@ public class Pbkdf2PasswordEncoder implements PasswordEncoder {
 		return result == 0;
 	}
 
+	private byte[] decode(String encodedBytes) {
+		if(this.encodeHashAsBase64) {
+			return Base64.getDecoder().decode(encodedBytes);
+		}
+		return Hex.decode(encodedBytes);
+	}
+
 	private byte[] encode(CharSequence rawPassword, byte[] salt) {
 		try {
 			PBEKeySpec spec = new PBEKeySpec(rawPassword.toString().toCharArray(),
 					concatenate(salt, this.secret), this.iterations, this.hashWidth);
-			SecretKeyFactory skf = SecretKeyFactory.getInstance(PBKDF2_ALGORITHM);
+			SecretKeyFactory skf = SecretKeyFactory.getInstance(this.algorithm);
 			return concatenate(salt, skf.generateSecret(spec).getEncoded());
 		}
 		catch (GeneralSecurityException e) {
 			throw new IllegalStateException("Could not create hash", e);
 		}
 	}
+
+	/**
+	 * The Algorithm used for creating the {@link SecretKeyFactory}
+	 *
+	 * @since 5.0
+	 */
+	public enum SecretKeyFactoryAlgorithm {
+		PBKDF2WithHmacSHA1,
+		PBKDF2WithHmacSHA256,
+		PBKDF2WithHmacSHA512
+	}
 }

+ 9 - 0
crypto/src/main/java/org/springframework/security/crypto/password/StandardPasswordEncoder.java

@@ -24,6 +24,9 @@ import org.springframework.security.crypto.keygen.BytesKeyGenerator;
 import org.springframework.security.crypto.keygen.KeyGenerators;
 
 /**
+ * This {@link PasswordEncoder} is provided for legacy purposes only and is not considered
+ * secure.
+ *
  * A standard {@code PasswordEncoder} implementation that uses SHA-256 hashing with 1024
  * iterations and a random 8-byte random salt value. It uses an additional system-wide
  * secret value to provide additional protection.
@@ -37,7 +40,13 @@ import org.springframework.security.crypto.keygen.KeyGenerators;
  *
  * @author Keith Donald
  * @author Luke Taylor
+ * @deprecated Digest based password encoding is not considered secure. Instead use an
+ * adaptive one way function like BCryptPasswordEncoder, Pbkdf2PasswordEncoder, or
+ * SCryptPasswordEncoder. Even better use {@link DelegatingPasswordEncoder} which supports
+ * password upgrades. There are no plans to remove this support. It is deprecated to indicate
+ * that this is a legacy implementation and using it is considered insecure.
  */
+@Deprecated
 public final class StandardPasswordEncoder implements PasswordEncoder {
 
 	private final Digester digester;

+ 1 - 1
crypto/src/test/java/org/springframework/security/crypto/codec/HexTests.java

@@ -34,7 +34,7 @@ public class HexTests {
 	@Test
 	public void encode() {
 		assertThat(Hex.encode(new byte[] { (byte) 'A', (byte) 'B', (byte) 'C',
-				(byte) 'D' })).isEqualTo(new char[] {'4','1','4','2','4','3','4','4'});
+				(byte) 'D' })).isEqualTo(new char[] {'4', '1', '4', '2', '4', '3', '4', '4'});
 	}
 
 	@Test

+ 101 - 0
crypto/src/test/java/org/springframework/security/crypto/factory/PasswordEncoderFactoriesTests.java

@@ -0,0 +1,101 @@
+/*
+ * Copyright 2002-2017 the original author or authors.
+ *
+ * Licensed 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.springframework.security.crypto.factory;
+
+import org.junit.Test;
+import org.springframework.security.crypto.password.PasswordEncoder;
+
+import static org.assertj.core.api.Assertions.*;
+
+/**
+ * @author Rob Winch
+ * @since 5.0
+ */
+public class PasswordEncoderFactoriesTests {
+	private PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
+
+	private String rawPassword = "password";
+
+	@Test
+	public void encodeWhenDefaultThenBCryptUsed() {
+		String encodedPassword = this.encoder.encode(this.rawPassword);
+
+		assertThat(encodedPassword).startsWith("{bcrypt}");
+		assertThat(this.encoder.matches(this.rawPassword, encodedPassword)).isTrue();
+	}
+
+	@Test
+	public void matchesWhenBCryptThenWorks() {
+		String encodedPassword = "{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG";
+		assertThat(this.encoder.matches(this.rawPassword, encodedPassword)).isTrue();
+	}
+
+	@Test
+	public void matchesWhenLdapThenWorks() {
+		String encodedPassword = "{ldap}{SSHA}igvD9lOiTXm16dmOw0YWRb9OjK2ThZvdQku2EQ==";
+		assertThat(this.encoder.matches(this.rawPassword, encodedPassword)).isTrue();
+	}
+
+	@Test
+	public void matchesWhenMd4ThenWorks() {
+		String encodedPassword = "{MD4}{KYp8/QErWyQemYazZQ8UnWWfbGbkYkVC8qMi0duoA84=}152ce09d3261d2b53cac55b2ea4d1c7a";
+		assertThat(this.encoder.matches(this.rawPassword, encodedPassword)).isTrue();
+	}
+
+	@Test
+	public void matchesWhenMd5ThenWorks() {
+		String encodedPassword = "{MD5}{aRYR+Yp2xSqtgF+vtjH6jNda6M083iEbP+zCFjLt9IA=}905e382a25eed53e22224223b3581092";
+		assertThat(this.encoder.matches(this.rawPassword, encodedPassword)).isTrue();
+	}
+
+	@Test
+	public void matchesWhenNoopThenWorks() {
+		String encodedPassword = "{noop}password";
+		assertThat(this.encoder.matches(this.rawPassword, encodedPassword)).isTrue();
+	}
+
+	@Test
+	public void matchesWhenPbkdf2ThenWorks() {
+		String encodedPassword = "{pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc";
+		assertThat(this.encoder.matches(this.rawPassword, encodedPassword)).isTrue();
+	}
+
+	@Test
+	public void matchesWhenSCryptThenWorks() {
+		String encodedPassword = "{scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=";
+		assertThat(this.encoder.matches(this.rawPassword, encodedPassword)).isTrue();
+	}
+
+	@Test
+	public void matchesWhenSHA1ThenWorks() {
+		String encodedPassword = "{SHA-1}{6581QepZz2qd8jVrT2QYPVtK8DuM2n45dVslmc3UTWc=}4f31573948ddbfb8ac9dd80107dfad13fd8f2454";
+		assertThat(this.encoder.matches(this.rawPassword, encodedPassword)).isTrue();
+	}
+
+	@Test
+	public void matchesWhenSHA256ThenWorks() {
+		String encodedPassword = "{SHA-256}{UisHp3pFSMqcqrhQsrhR+hspIG0SyMDyDW/XtY+t6nA=}a98efbaf59277bfd1837c33fd4fde67de5bcfd2205bcba0992f6fc32b03a8f88";
+		assertThat(this.encoder.matches(this.rawPassword, encodedPassword)).isTrue();
+	}
+
+	@Test
+	public void matchesWhenSha256ThenWorks() {
+		String encodedPassword = "{sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0";
+		assertThat(this.encoder.matches(this.rawPassword, encodedPassword)).isTrue();
+	}
+
+}

+ 66 - 0
crypto/src/test/java/org/springframework/security/crypto/keygen/Base64StringKeyGeneratorTests.java

@@ -0,0 +1,66 @@
+/*
+ * Copyright 2002-2017 the original author or authors.
+ *
+ * Licensed 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.springframework.security.crypto.keygen;
+
+import org.junit.Test;
+
+import java.util.Base64;
+
+import static org.assertj.core.api.Assertions.*;
+
+/**
+ * @author Rob Winch
+ * @since 5.0
+ */
+public class Base64StringKeyGeneratorTests {
+	@Test(expected = IllegalArgumentException.class)
+	public void constructorIntWhenLessThan32ThenIllegalArgumentException() {
+		new Base64StringKeyGenerator(31);
+	}
+
+	@Test(expected = IllegalArgumentException.class)
+	public void constructorEncoderWhenEncoderNullThenThrowsIllegalArgumentException() {
+		Base64.Encoder encoder = null;
+		new Base64StringKeyGenerator(null);
+	}
+
+	@Test
+	public void generateKeyWhenDefaultConstructorThen32Bytes() {
+		String result = new Base64StringKeyGenerator().generateKey();
+		assertThat(Base64.getDecoder().decode(result.getBytes())).hasSize(32);
+	}
+
+	@Test
+	public void generateKeyWhenCustomKeySizeThen32Bytes() {
+		int size = 40;
+		String result = new Base64StringKeyGenerator(size).generateKey();
+		assertThat(Base64.getDecoder().decode(result.getBytes())).hasSize(size);
+	}
+
+	@Test
+	public void generateKeyWhenBase64Then32Bytes() {
+		String result = new Base64StringKeyGenerator(Base64.getUrlEncoder()).generateKey();
+		assertThat(Base64.getUrlDecoder().decode(result.getBytes())).hasSize(32);
+	}
+
+	@Test
+	public void generateKeyWhenBase64AndCustomKeySizeThen32Bytes() {
+		int size = 40;
+		String result = new Base64StringKeyGenerator(Base64.getUrlEncoder(), size).generateKey();
+		assertThat(Base64.getUrlDecoder().decode(result.getBytes())).hasSize(size);
+	}
+}

+ 219 - 0
crypto/src/test/java/org/springframework/security/crypto/password/DelegatingPasswordEncoderTests.java

@@ -0,0 +1,219 @@
+/*
+ * Copyright 2002-2017 the original author or authors.
+ *
+ * Licensed 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.springframework.security.crypto.password;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.fail;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.Map;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+
+/**
+ * @author Rob Winch
+ * @author Michael Simons
+ * @since 5.0
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class DelegatingPasswordEncoderTests {
+	@Mock
+	private PasswordEncoder bcrypt;
+
+	@Mock
+	private PasswordEncoder noop;
+
+	@Mock
+	private PasswordEncoder invalidId;
+
+	private String bcryptId = "bcrypt";
+
+	private String rawPassword = "password";
+
+	private String encodedPassword = "ENCODED-PASSWORD";
+
+	private String bcryptEncodedPassword = "{bcrypt}" + this.encodedPassword;
+
+	private String noopEncodedPassword = "{noop}" + this.encodedPassword;
+
+	private Map<String, PasswordEncoder> delegates;
+
+	private DelegatingPasswordEncoder passwordEncoder;
+
+	@Before
+	public void setup() {
+		this.delegates = new HashMap<String, PasswordEncoder>();
+		this.delegates.put(this.bcryptId, this.bcrypt);
+		this.delegates.put("noop", this.noop);
+
+		this.passwordEncoder = new DelegatingPasswordEncoder(this.bcryptId, this.delegates);
+	}
+
+	@Test(expected = IllegalArgumentException.class)
+	public void constructorWhenIdForEncodeNullThenIllegalArgumentException() {
+		new DelegatingPasswordEncoder(null, this.delegates);
+	}
+
+	@Test(expected = IllegalArgumentException.class)
+	public void constructorWhenIdForEncodeDoesNotExistThenIllegalArgumentException() {
+		new DelegatingPasswordEncoder(this.bcryptId + "INVALID", this.delegates);
+	}
+
+	@Test(expected = IllegalArgumentException.class)
+	public void setDefaultPasswordEncoderForMatchesWhenNullThenIllegalArgumentException() {
+		this.passwordEncoder.setDefaultPasswordEncoderForMatches(null);
+	}
+
+	@Test
+	public void matchesWhenCustomDefaultPasswordEncoderForMatchesThenDelegates() {
+		String encodedPassword = "{unmapped}" + this.rawPassword;
+		this.passwordEncoder.setDefaultPasswordEncoderForMatches(this.invalidId);
+
+		assertThat(this.passwordEncoder.matches(this.rawPassword, encodedPassword)).isFalse();
+
+		verify(this.invalidId).matches(this.rawPassword, encodedPassword);
+		verifyZeroInteractions(this.bcrypt, this.noop);
+	}
+
+	@Test
+	public void encodeWhenValidThenUsesIdForEncode() {
+		when(this.bcrypt.encode(this.rawPassword)).thenReturn(this.encodedPassword);
+
+		assertThat(this.passwordEncoder.encode(this.rawPassword)).isEqualTo(this.bcryptEncodedPassword);
+	}
+
+	@Test
+	public void matchesWhenBCryptThenDelegatesToBCrypt() {
+		when(this.bcrypt.matches(this.rawPassword, this.encodedPassword)).thenReturn(true);
+
+		assertThat(this.passwordEncoder.matches(this.rawPassword, this.bcryptEncodedPassword)).isTrue();
+
+		verify(this.bcrypt).matches(this.rawPassword, this.encodedPassword);
+		verifyZeroInteractions(this.noop);
+	}
+
+	@Test
+	public void matchesWhenNoopThenDelegatesToNoop() {
+		when(this.noop.matches(this.rawPassword, this.encodedPassword)).thenReturn(true);
+
+		assertThat(this.passwordEncoder.matches(this.rawPassword, this.noopEncodedPassword)).isTrue();
+
+		verify(this.noop).matches(this.rawPassword, this.encodedPassword);
+		verifyZeroInteractions(this.bcrypt);
+	}
+
+	@Test
+	public void matchesWhenUnMappedThenIllegalArgumentException() {
+		try {
+			this.passwordEncoder.matches(this.rawPassword, "{unmapped}" + this.rawPassword);
+			fail("Expected Exception");
+		} catch(IllegalArgumentException e) {
+			assertThat(e).hasMessage("There is no PasswordEncoder mapped for the id \"unmapped\"");
+		}
+
+		verifyZeroInteractions(this.bcrypt, this.noop);
+	}
+
+	@Test
+	public void matchesWhenNoClosingPrefixStringThenIllegalArgumentExcetion() {
+		try {
+			this.passwordEncoder.matches(this.rawPassword, "{bcrypt" + this.rawPassword);
+			fail("Expected Exception");
+		} catch(IllegalArgumentException e) {
+			assertThat(e).hasMessage("There is no PasswordEncoder mapped for the id \"null\"");
+		}
+
+		verifyZeroInteractions(this.bcrypt, this.noop);
+	}
+
+	@Test
+	public void matchesWhenNoStartingPrefixStringThenFalse() {
+		try {
+			this.passwordEncoder.matches(this.rawPassword, "bcrypt}" + this.rawPassword);
+			fail("Expected Exception");
+		} catch(IllegalArgumentException e) {
+			assertThat(e).hasMessage("There is no PasswordEncoder mapped for the id \"null\"");
+		}
+
+		verifyZeroInteractions(this.bcrypt, this.noop);
+	}
+
+	@Test
+	public void matchesWhenNoIdStringThenFalse() {
+		try {
+			this.passwordEncoder.matches(this.rawPassword, "{}" + this.rawPassword);
+			fail("Expected Exception");
+		} catch(IllegalArgumentException e) {
+			assertThat(e).hasMessage("There is no PasswordEncoder mapped for the id \"\"");
+		}
+
+		verifyZeroInteractions(this.bcrypt, this.noop);
+	}
+
+	@Test
+	public void matchesWhenPrefixInMiddleThenFalse() {
+		try {
+			this.passwordEncoder.matches(this.rawPassword, "invalid" + this.bcryptEncodedPassword);
+			fail("Expected Exception");
+		} catch(IllegalArgumentException e) {
+			assertThat(e).hasMessage("There is no PasswordEncoder mapped for the id \"null\"");
+		}
+
+		verifyZeroInteractions(this.bcrypt, this.noop);
+	}
+
+	@Test
+	public void matchesWhenIdIsNullThenFalse() {
+		this.delegates = new Hashtable<String, PasswordEncoder>(this.delegates);
+
+		DelegatingPasswordEncoder passwordEncoder = new DelegatingPasswordEncoder(this.bcryptId, this.delegates);
+
+		try {
+			passwordEncoder.matches(this.rawPassword, this.rawPassword);
+			fail("Expected Exception");
+		} catch(IllegalArgumentException e) {
+			assertThat(e).hasMessage("There is no PasswordEncoder mapped for the id \"null\"");
+		}
+
+		verifyZeroInteractions(this.bcrypt, this.noop);
+	}
+
+	@Test
+	public void matchesWhenNullIdThenDelegatesToInvalidId() {
+		this.delegates.put(null, this.invalidId);
+		this.passwordEncoder = new DelegatingPasswordEncoder(this.bcryptId, this.delegates);
+		when(this.invalidId.matches(this.rawPassword, this.encodedPassword)).thenReturn(true);
+
+		assertThat(this.passwordEncoder.matches(this.rawPassword, this.encodedPassword)).isTrue();
+
+		verify(this.invalidId).matches(this.rawPassword, this.encodedPassword);
+		verifyZeroInteractions(this.bcrypt, this.noop);
+	}
+
+	@Test(expected = IllegalArgumentException.class)
+	public void matchesWhenRawPasswordNotNullAndEncodedPasswordNullThenThrowsIllegalArgumentException() {
+		this.passwordEncoder.matches(this.rawPassword, null);
+	}
+}

+ 111 - 0
crypto/src/test/java/org/springframework/security/crypto/password/LdapShaPasswordEncoderTests.java

@@ -0,0 +1,111 @@
+/*
+ * Copyright 2002-2017 the original author or authors.
+ *
+ * Licensed 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.springframework.security.crypto.password;
+
+import org.junit.Test;
+import org.springframework.security.crypto.keygen.KeyGenerators;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests {@link LdapShaPasswordEncoder}.
+ *
+ * @author Luke Taylor
+ */
+public class LdapShaPasswordEncoderTests {
+	// ~ Instance fields
+	// ================================================================================================
+
+	LdapShaPasswordEncoder sha = new LdapShaPasswordEncoder();
+
+	// ~ Methods
+	// ========================================================================================================
+
+	@Test
+	public void invalidPasswordFails() {
+		assertThat(this.sha.matches("wrongpassword", "{SHA}ddSFGmjXYPbZC+NXR2kCzBRjqiE=")).isFalse();
+	}
+
+	@Test
+	public void invalidSaltedPasswordFails() {
+		assertThat(this.sha.matches("wrongpassword", "{SSHA}25ro4PKC8jhQZ26jVsozhX/xaP0suHgX")).isFalse();
+		assertThat(this.sha.matches("wrongpassword", "{SSHA}PQy2j+6n5ytA+YlAKkM8Fh4p6u2JxfVd")).isFalse();
+	}
+
+	/**
+	 * Test values generated by 'slappasswd -h {SHA} -s boabspasswurd'
+	 */
+	@Test
+	public void validPasswordSucceeds() {
+		this.sha.setForceLowerCasePrefix(false);
+		assertThat(this.sha.matches("boabspasswurd", "{SHA}ddSFGmjXYPbZC+NXR2kCzBRjqiE=")).isTrue();
+		assertThat(this.sha.matches("boabspasswurd", "{sha}ddSFGmjXYPbZC+NXR2kCzBRjqiE=")).isTrue();
+		this.sha.setForceLowerCasePrefix(true);
+		assertThat(this.sha.matches("boabspasswurd", "{SHA}ddSFGmjXYPbZC+NXR2kCzBRjqiE=")).isTrue();
+		assertThat(this.sha.matches("boabspasswurd", "{sha}ddSFGmjXYPbZC+NXR2kCzBRjqiE=")).isTrue();
+	}
+
+	/**
+	 * Test values generated by 'slappasswd -s boabspasswurd'
+	 */
+	@Test
+	public void validSaltedPasswordSucceeds() {
+		this.sha.setForceLowerCasePrefix(false);
+		assertThat(this.sha.matches("boabspasswurd", "{SSHA}25ro4PKC8jhQZ26jVsozhX/xaP0suHgX")).isTrue();
+		assertThat(this.sha.matches("boabspasswurd", "{ssha}PQy2j+6n5ytA+YlAKkM8Fh4p6u2JxfVd")).isTrue();
+		this.sha.setForceLowerCasePrefix(true);
+		assertThat(this.sha.matches("boabspasswurd", "{SSHA}25ro4PKC8jhQZ26jVsozhX/xaP0suHgX")).isTrue();
+		assertThat(this.sha.matches("boabspasswurd", "{ssha}PQy2j+6n5ytA+YlAKkM8Fh4p6u2JxfVd")).isTrue();
+	}
+
+	@Test
+	// SEC-1031
+	public void fullLengthOfHashIsUsedInComparison() throws Exception {
+		assertThat(this.sha.matches("boabspasswurd", "{SSHA}25ro4PKC8jhQZ26jVsozhX/xaP0suHgX")).isTrue();
+		// Change the first hash character from '2' to '3'
+		assertThat(this.sha.matches("boabspasswurd", "{SSHA}35ro4PKC8jhQZ26jVsozhX/xaP0suHgX")).isFalse();
+		// Change the last hash character from 'X' to 'Y'
+		assertThat(this.sha.matches("boabspasswurd", "{SSHA}25ro4PKC8jhQZ26jVsozhX/xaP0suHgY")).isFalse();
+	}
+
+	@Test
+	public void correctPrefixCaseIsUsed() {
+		this.sha.setForceLowerCasePrefix(false);
+		assertThat(this.sha.encode("somepassword").startsWith("{SSHA}"));
+
+		this.sha.setForceLowerCasePrefix(true);
+		assertThat(this.sha.encode("somepassword").startsWith("{ssha}"));
+
+		this.sha = new LdapShaPasswordEncoder(KeyGenerators.shared(0));
+		this.sha.setForceLowerCasePrefix(false);
+		assertThat(this.sha.encode("somepassword").startsWith("{SHA}"));
+
+		this.sha.setForceLowerCasePrefix(true);
+		assertThat(this.sha.encode("somepassword").startsWith("{SSHA}"));
+	}
+
+	@Test(expected = IllegalArgumentException.class)
+	public void invalidPrefixIsRejected() {
+		this.sha.matches("somepassword", "{MD9}xxxxxxxxxx");
+	}
+
+	@Test(expected = IllegalArgumentException.class)
+	public void malformedPrefixIsRejected() {
+		// No right brace
+		this.sha.matches("somepassword", "{SSHA25ro4PKC8jhQZ26jVsozhX/xaP0suHgX");
+	}
+}

+ 76 - 0
crypto/src/test/java/org/springframework/security/crypto/password/Md4PasswordEncoderTests.java

@@ -0,0 +1,76 @@
+/*
+ * Copyright 2002-2017 the original author or authors.
+ *
+ * Licensed 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.springframework.security.crypto.password;
+
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.junit.Test;
+
+
+public class Md4PasswordEncoderTests {
+
+	@Test
+	public void testEncodeUnsaltedPassword() {
+		Md4PasswordEncoder md4 = new Md4PasswordEncoder();
+		md4.setEncodeHashAsBase64(true);
+		assertThat(md4.matches("ww_uni123", "8zobtq72iAt0W6KNqavGwg==")).isTrue();
+	}
+
+	@Test
+	public void testEncodeSaltedPassword() {
+		Md4PasswordEncoder md4 = new Md4PasswordEncoder();
+		md4.setEncodeHashAsBase64(true);
+		assertThat(md4.matches("ww_uni123", "{Alan K Stewart}ZplT6P5Kv6Rlu6W4FIoYNA==")).isTrue();
+	}
+
+	@Test
+	public void testEncodeNullPassword() {
+		Md4PasswordEncoder md4 = new Md4PasswordEncoder();
+		md4.setEncodeHashAsBase64(true);
+		assertThat(md4.matches(null, "MdbP4NFq6TG3PFnX4MCJwA==")).isTrue();
+	}
+
+	@Test
+	public void testEncodeEmptyPassword() {
+		Md4PasswordEncoder md4 = new Md4PasswordEncoder();
+		md4.setEncodeHashAsBase64(true);
+		assertThat(md4.matches(null, "MdbP4NFq6TG3PFnX4MCJwA==")).isTrue();
+	}
+
+	@Test
+	public void testNonAsciiPasswordHasCorrectHash() {
+		Md4PasswordEncoder md4 = new Md4PasswordEncoder();
+		assertThat(md4.matches("\u4F60\u597d", "a7f1196539fd1f85f754ffd185b16e6e")).isTrue();
+	}
+
+	@Test
+	public void testEncodedMatches() {
+		String rawPassword = "password";
+		Md4PasswordEncoder md4 = new Md4PasswordEncoder();
+		String encodedPassword = md4.encode(rawPassword);
+
+		assertThat(md4.matches(rawPassword, encodedPassword)).isTrue();
+	}
+
+	@Test
+	public void javadocWhenHasSaltThenMatches() {
+		Md4PasswordEncoder encoder = new Md4PasswordEncoder();
+		assertThat(encoder.matches("password", "{thisissalt}6cc7924dad12ade79dfb99e424f25260"));
+	}
+}
+

+ 121 - 0
crypto/src/test/java/org/springframework/security/crypto/password/MessageDigestPasswordEncoderTests.java

@@ -0,0 +1,121 @@
+/*
+ * Copyright 2002-2017 the original author or authors.
+ *
+ * Licensed 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.springframework.security.crypto.password;
+
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * <p>
+ * TestCase for Md5PasswordEncoder.
+ * </p>
+ *
+ * @author colin sampaleanu
+ * @author Ben Alex
+ * @author Ray Krueger
+ * @author Luke Taylor
+ */
+public class MessageDigestPasswordEncoderTests {
+	// ~ Methods
+	// ========================================================================================================
+
+	@Test
+	public void md5BasicFunctionality() {
+		MessageDigestPasswordEncoder pe = new MessageDigestPasswordEncoder("MD5");
+		String raw = "abc123";
+		assertThat(pe.matches( raw, "{THIS_IS_A_SALT}a68aafd90299d0b137de28fb4bb68573")).isTrue();
+	}
+
+	@Test
+	public void md5NonAsciiPasswordHasCorrectHash() throws Exception {
+		MessageDigestPasswordEncoder pe = new MessageDigestPasswordEncoder("MD5");
+		// $ echo -n "??" | md5
+		// 7eca689f0d3389d9dea66ae112e5cfd7
+		assertThat(pe.matches("\u4F60\u597d", "7eca689f0d3389d9dea66ae112e5cfd7")).isTrue();
+	}
+
+	@Test
+	public void md5Base64() throws Exception {
+		MessageDigestPasswordEncoder pe = new MessageDigestPasswordEncoder("MD5");
+		pe.setEncodeHashAsBase64(true);
+		assertThat(pe.matches("abc123", "{THIS_IS_A_SALT}poqv2QKZ0LE33ij7S7aFcw==")).isTrue();
+	}
+
+	@Test
+	public void md5StretchFactorIsProcessedCorrectly() throws Exception {
+		MessageDigestPasswordEncoder pe = new MessageDigestPasswordEncoder("MD5");
+		pe.setIterations(2);
+		// Calculate value using:
+		// echo -n password{salt} | openssl md5 -binary | openssl md5
+		assertThat(pe.matches("password", "{salt}eb753fb0c370582b4ee01b30f304b9fc")).isTrue();
+	}
+
+	@Test
+	public void md5MatchesWhenNullSalt() {
+		MessageDigestPasswordEncoder pe = new MessageDigestPasswordEncoder("MD5");
+		assertThat(pe.matches("password", "5f4dcc3b5aa765d61d8327deb882cf99")).isTrue();
+	}
+
+	@Test
+	public void md5MatchesWhenEmptySalt() {
+		MessageDigestPasswordEncoder pe = new MessageDigestPasswordEncoder("MD5");
+		assertThat(pe.matches("password", "{}f1026a66095fc2058c1f8771ed05d6da")).isTrue();
+	}
+
+	@Test
+	public void md5MatchesWhenHasSalt() {
+		MessageDigestPasswordEncoder pe = new MessageDigestPasswordEncoder("MD5");
+		assertThat(pe.matches("password", "{salt}ce421738b1c5540836bdc8ff707f1572")).isTrue();
+	}
+
+	@Test
+	public void md5EncodeThenMatches() {
+		String rawPassword = "password";
+		MessageDigestPasswordEncoder pe = new MessageDigestPasswordEncoder("MD5");
+		String encode = pe.encode(rawPassword);
+		assertThat(pe.matches(rawPassword, encode)).isTrue();
+	}
+
+	@Test
+	public void testBasicFunctionality() {
+		MessageDigestPasswordEncoder pe = new MessageDigestPasswordEncoder("SHA-1");
+		String raw = "abc123";
+		assertThat(pe.matches(raw, "{THIS_IS_A_SALT}b2f50ffcbd3407fe9415c062d55f54731f340d32"));
+
+	}
+
+	@Test
+	public void testBase64() throws Exception {
+		MessageDigestPasswordEncoder pe = new MessageDigestPasswordEncoder("SHA-1");
+		pe.setEncodeHashAsBase64(true);
+		String raw = "abc123";
+		assertThat(pe.matches(raw, "{THIS_IS_A_SALT}b2f50ffcbd3407fe9415c062d55f54731f340d32"));
+	}
+
+	@Test
+	public void test256() throws Exception {
+		MessageDigestPasswordEncoder pe = new MessageDigestPasswordEncoder("SHA-1");
+		String raw = "abc123";
+		assertThat(pe.matches(raw, "{THIS_IS_A_SALT}4b79b7de23eb23b78cc5ede227d532b8a51f89b2ec166f808af76b0dbedc47d7"));
+	}
+
+	@Test(expected = IllegalStateException.class)
+	public void testInvalidStrength() throws Exception {
+		new MessageDigestPasswordEncoder("SHA-666");
+	}
+}

+ 70 - 0
crypto/src/test/java/org/springframework/security/crypto/password/PasswordEncoderUtilsTests.java

@@ -0,0 +1,70 @@
+/*
+ * Copyright 2002-2017 the original author or authors.
+ *
+ * Licensed 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.springframework.security.crypto.password;
+
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Rob Winch
+ */
+public class PasswordEncoderUtilsTests {
+
+	@Test
+	public void equalsWhenDifferentLengthThenFalse() {
+		assertThat(PasswordEncoderUtils.equals("abc", "a")).isFalse();
+		assertThat(PasswordEncoderUtils.equals("a", "abc")).isFalse();
+	}
+
+	@Test
+	public void equalsWhenNullAndNotEmtpyThenFalse() {
+		assertThat(PasswordEncoderUtils.equals(null, "a")).isFalse();
+		assertThat(PasswordEncoderUtils.equals("a", null)).isFalse();
+	}
+
+	@Test
+	public void equalsWhenNullAndNullThenTrue() {
+		assertThat(PasswordEncoderUtils.equals(null, null)).isTrue();
+	}
+
+	@Test
+	public void equalsWhenNullAndEmptyThenFalse() {
+		assertThat(PasswordEncoderUtils.equals(null, "")).isFalse();
+		assertThat(PasswordEncoderUtils.equals("", null)).isFalse();
+	}
+
+	@Test
+	public void equalsWhenNotEmptyAndEmptyThenFalse() {
+		assertThat(PasswordEncoderUtils.equals("abc", "")).isFalse();
+		assertThat(PasswordEncoderUtils.equals("", "abc")).isFalse();
+	}
+
+	@Test
+	public void equalsWhenEmtpyAndEmptyThenTrue() {
+		assertThat(PasswordEncoderUtils.equals("", "")).isTrue();
+	}
+
+	@Test
+	public void equalsWhenDifferentCaseThenFalse() {
+		assertThat(PasswordEncoderUtils.equals("aBc", "abc")).isFalse();
+	}
+
+	@Test
+	public void equalsWhenSameThenTrue() {
+		assertThat(PasswordEncoderUtils.equals("abcdef", "abcdef")).isTrue();
+	}
+}

+ 37 - 1
crypto/src/test/java/org/springframework/security/crypto/password/Pbkdf2PasswordEncoderTests.java

@@ -75,6 +75,42 @@ public class Pbkdf2PasswordEncoderTests {
 		assertThat(fixedHex).isEqualTo(encodedPassword);
 	}
 
+	@Test
+	public void encodeAndMatchWhenBase64ThenSuccess() {
+		this.encoder.setEncodeHashAsBase64(true);
+
+		String rawPassword = "password";
+		String encodedPassword = this.encoder.encode(rawPassword);
+		assertThat(this.encoder.matches(rawPassword, encodedPassword)).isTrue();
+	}
+
+	@Test
+	public void matchWhenBase64ThenSuccess() {
+		this.encoder.setEncodeHashAsBase64(true);
+		String rawPassword = "password";
+		String encodedPassword = "3FOwOMcDgxP+z1x/sv184LFY2WVD+ZGMgYP3LPOSmCcDmk1XPYvcCQ==";
+
+		assertThat(this.encoder.matches(rawPassword, encodedPassword)).isTrue();
+		java.util.Base64.getDecoder().decode(encodedPassword); // validate can decode as Base64
+	}
+
+	@Test
+	public void encodeAndMatchWhenSha256ThenSuccess() {
+		this.encoder.setAlgorithm(Pbkdf2PasswordEncoder.SecretKeyFactoryAlgorithm.PBKDF2WithHmacSHA256);
+
+		String rawPassword = "password";
+		String encodedPassword = this.encoder.encode(rawPassword);
+		assertThat(this.encoder.matches(rawPassword, encodedPassword)).isTrue();
+	}
+
+	@Test
+	public void matchWhenSha256ThenSuccess() {
+		this.encoder.setAlgorithm(Pbkdf2PasswordEncoder.SecretKeyFactoryAlgorithm.PBKDF2WithHmacSHA256);
+
+		String rawPassword = "password";
+		String encodedPassword = "821447f994e2b04c5014e31fa9fca4ae1cc9f2188c4ed53d3ddb5ba7980982b51a0ecebfc0b81a79";
+		assertThat(this.encoder.matches(rawPassword, encodedPassword)).isTrue();
+	}
 	/**
 	 * Used to find the iteration count that takes .5 seconds.
 	 */
@@ -105,4 +141,4 @@ public class Pbkdf2PasswordEncoderTests {
 		}
 		System.out.println("Iterations " + iterations);
 	}
-}
+}

+ 15 - 0
crypto/src/test/resources/logback-test.xml

@@ -0,0 +1,15 @@
+<configuration>
+	<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+	<encoder>
+		<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
+	</encoder>
+	</appender>
+
+	<logger name="org.springframework.security" level="${sec.log.level:-WARN}"/>
+
+
+	<root level="${root.level:-WARN}">
+	<appender-ref ref="STDOUT" />
+	</root>
+
+</configuration>