diff --git a/core/src/main/java/org/springframework/security/authentication/encoding/BaseDigestPasswordEncoder.java b/core/src/main/java/org/springframework/security/authentication/encoding/BaseDigestPasswordEncoder.java index d8195d0d9e..533d4944ab 100644 --- a/core/src/main/java/org/springframework/security/authentication/encoding/BaseDigestPasswordEncoder.java +++ b/core/src/main/java/org/springframework/security/authentication/encoding/BaseDigestPasswordEncoder.java @@ -22,7 +22,10 @@ package org.springframework.security.authentication.encoding; *

* * @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 // ================================================================================================ diff --git a/core/src/main/java/org/springframework/security/authentication/encoding/BasePasswordEncoder.java b/core/src/main/java/org/springframework/security/authentication/encoding/BasePasswordEncoder.java index ee8d2d3f94..83586b8667 100644 --- a/core/src/main/java/org/springframework/security/authentication/encoding/BasePasswordEncoder.java +++ b/core/src/main/java/org/springframework/security/authentication/encoding/BasePasswordEncoder.java @@ -22,7 +22,10 @@ package org.springframework.security.authentication.encoding; *

* * @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 // ======================================================================================================== diff --git a/core/src/main/java/org/springframework/security/authentication/encoding/LdapShaPasswordEncoder.java b/core/src/main/java/org/springframework/security/authentication/encoding/LdapShaPasswordEncoder.java index 5ba2fff772..96a7d94fbb 100644 --- a/core/src/main/java/org/springframework/security/authentication/encoding/LdapShaPasswordEncoder.java +++ b/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 diff --git a/core/src/main/java/org/springframework/security/authentication/encoding/Md4PasswordEncoder.java b/core/src/main/java/org/springframework/security/authentication/encoding/Md4PasswordEncoder.java index 433960349c..211ac337fb 100644 --- a/core/src/main/java/org/springframework/security/authentication/encoding/Md4PasswordEncoder.java +++ b/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 diff --git a/core/src/main/java/org/springframework/security/authentication/encoding/Md5PasswordEncoder.java b/core/src/main/java/org/springframework/security/authentication/encoding/Md5PasswordEncoder.java index 20724dcaa3..014ef55ccf 100644 --- a/core/src/main/java/org/springframework/security/authentication/encoding/Md5PasswordEncoder.java +++ b/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() { diff --git a/core/src/main/java/org/springframework/security/authentication/encoding/MessageDigestPasswordEncoder.java b/core/src/main/java/org/springframework/security/authentication/encoding/MessageDigestPasswordEncoder.java index f1d8284759..1bdc4fbc56 100644 --- a/core/src/main/java/org/springframework/security/authentication/encoding/MessageDigestPasswordEncoder.java +++ b/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; diff --git a/core/src/main/java/org/springframework/security/authentication/encoding/PlaintextPasswordEncoder.java b/core/src/main/java/org/springframework/security/authentication/encoding/PlaintextPasswordEncoder.java index 9e49dc18bd..b7adee76bc 100644 --- a/core/src/main/java/org/springframework/security/authentication/encoding/PlaintextPasswordEncoder.java +++ b/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 // ================================================================================================ diff --git a/core/src/main/java/org/springframework/security/authentication/encoding/ShaPasswordEncoder.java b/core/src/main/java/org/springframework/security/authentication/encoding/ShaPasswordEncoder.java index c4ddf73acc..68dec99c1c 100644 --- a/core/src/main/java/org/springframework/security/authentication/encoding/ShaPasswordEncoder.java +++ b/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 { /** diff --git a/crypto/src/main/java/org/springframework/security/crypto/factory/PasswordEncoderFactories.java b/crypto/src/main/java/org/springframework/security/crypto/factory/PasswordEncoderFactories.java new file mode 100644 index 0000000000..118512a1e9 --- /dev/null +++ b/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: + * + * + * + * @return the {@link PasswordEncoder} to use + */ + public static PasswordEncoder createDelegatingPasswordEncoder() { + String encodingId = "bcrypt"; + Map encoders = new HashMap(); + 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() {} +} diff --git a/crypto/src/main/java/org/springframework/security/crypto/keygen/Base64StringKeyGenerator.java b/crypto/src/main/java/org/springframework/security/crypto/keygen/Base64StringKeyGenerator.java new file mode 100644 index 0000000000..469b00ed76 --- /dev/null +++ b/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); + } +} diff --git a/crypto/src/main/java/org/springframework/security/crypto/password/DelegatingPasswordEncoder.java b/crypto/src/main/java/org/springframework/security/crypto/password/DelegatingPasswordEncoder.java new file mode 100644 index 0000000000..7c62241b81 --- /dev/null +++ b/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. + * + *

Constructing an instance

+ * + * You can easily construct an instance using + * {@link org.springframework.security.crypto.factory.PasswordEncoderFactories}. + * Alternatively, you may create your own custom instance. For example: + * + *
+ * String idForEncode = "bcrypt";
+ * Map 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);
+ * 
+ * + * + *

Password Storage Format

+ * + * The general format for a password is: + * + *
+ * {id}encodedPassword
+ * 
+ * + * 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". + * + *
+ * {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
+ * {noop}password
+ * {pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc
+ * {scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=
+ * {sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0
+ * 
+ * + * For the DelegatingPasswordEncoder that we constructed above: + * + *
    + *
  1. 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}
  2. + *
  3. The second password would have a {@code PasswordEncoder} id of "noop" and + * encodedPassword of "password". When matching it would delegate to + * {@link NoOpPasswordEncoder}
  4. + *
  5. The third password would have a {@code PasswordEncoder} id of "pbkdf2" and + * encodedPassword of + * "5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc". + * When matching it would delegate to {@link Pbkdf2PasswordEncoder}
  6. + *
  7. 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}
  8. + *
  9. The final password would have a {@code PasswordEncoder} id of "sha256" and + * encodedPassword of + * "97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0". + * When matching it would delegate to {@link StandardPasswordEncoder}
  10. + *
+ * + *

Password Encoding

+ * + * 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: + * + *
+ * {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
+ * 
+ * + *

Password Matching

+ * + * 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 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 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(idToPasswordEncoder); + } + + /** + * Sets the {@link PasswordEncoder} to delegate to for + * {@link #matches(CharSequence, String)} if the id is not mapped to a + * {@link PasswordEncoder}. + * + *

+ 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". + *

+ * @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 + "\""); + } + } +} diff --git a/crypto/src/main/java/org/springframework/security/crypto/password/Digester.java b/crypto/src/main/java/org/springframework/security/crypto/password/Digester.java index d42ec3a821..35e9b600de 100644 --- a/crypto/src/main/java/org/springframework/security/crypto/password/Digester.java +++ b/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); diff --git a/crypto/src/main/java/org/springframework/security/crypto/password/LdapShaPasswordEncoder.java b/crypto/src/main/java/org/springframework/security/crypto/password/LdapShaPasswordEncoder.java new file mode 100644 index 0000000000..b7bc73bb0e --- /dev/null +++ b/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 forceLowerCasePrefix 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; + } +} diff --git a/crypto/src/main/java/org/springframework/security/crypto/password/Md4.java b/crypto/src/main/java/org/springframework/security/crypto/password/Md4.java new file mode 100644 index 0000000000..4bea3dab3c --- /dev/null +++ b/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); + } +} diff --git a/crypto/src/main/java/org/springframework/security/crypto/password/Md4PasswordEncoder.java b/crypto/src/main/java/org/springframework/security/crypto/password/Md4PasswordEncoder.java new file mode 100644 index 0000000000..33d64a8ec0 --- /dev/null +++ b/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: + * + *
+ * s = salt == null ? "" : "{" + salt + "}"
+ * s + md4(password + s)
+ * 
+ * + * 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": + * + *
+ * String s = salt == null ? "" : "{" + salt + "}";
+ * s + md4(password + s)
+ * "{thisissalt}" + md4(password + "{thisissalt}")
+ * "{thisissalt}6cc7924dad12ade79dfb99e424f25260"
+ * 
+ * + * If the salt does not exist, then omit "{salt}" like this: + * + *
+ * md4(password)
+ * 
+ * + * If the salt is an empty String, then only use "{}" like this: + * + *
+ * "{}" + md4(password + "{}")
+ * 
+ * + * 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: + * + *
+ * String salt = saltSource.getSalt(user);
+ * String s = salt == null ? null : "{" + salt + "}";
+ * String migratedPassword = s + user.getPassword();
+ * 
+ * + * @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); + } +} diff --git a/crypto/src/main/java/org/springframework/security/crypto/password/MessageDigestPasswordEncoder.java b/crypto/src/main/java/org/springframework/security/crypto/password/MessageDigestPasswordEncoder.java new file mode 100644 index 0000000000..eb746076f4 --- /dev/null +++ b/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: + * + *
+ * s = salt == null ? "" : "{" + salt + "}"
+ * s + digest(password + s)
+ * 
+ * + * 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": + * + *
+ * String s = salt == null ? "" : "{" + salt + "}";
+ * s + md5(password + s)
+ * "{thisissalt}" + md5(password + "{thisissalt}")
+ * "{thisissalt}2a4e7104c2780098f50ed5a84bb2323d"
+ * 
+ * + * If the salt does not exist, then omit "{salt}" like this: + * + *
+ * digest(password)
+ * 
+ * + * If the salt is an empty String, then only use "{}" like this: + * + *
+ * "{}" + digest(password + "{}")
+ * 
+ * + * 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: + * + *
+ * String salt = saltSource.getSalt(user);
+ * String s = salt == null ? null : "{" + salt + "}";
+ * String migratedPassword = s + user.getPassword();
+ * 
+ * + * @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 + * + * Message Digest Algorithms 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); + } +} diff --git a/crypto/src/main/java/org/springframework/security/crypto/password/NoOpPasswordEncoder.java b/crypto/src/main/java/org/springframework/security/crypto/password/NoOpPasswordEncoder.java index 9de708bd33..696a89b4b8 100644 --- a/crypto/src/main/java/org/springframework/security/crypto/password/NoOpPasswordEncoder.java +++ b/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) { diff --git a/crypto/src/main/java/org/springframework/security/crypto/password/PasswordEncoderUtils.java b/crypto/src/main/java/org/springframework/security/crypto/password/PasswordEncoderUtils.java new file mode 100644 index 0000000000..aca4b7681d --- /dev/null +++ b/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() { + } +} diff --git a/crypto/src/main/java/org/springframework/security/crypto/password/Pbkdf2PasswordEncoder.java b/crypto/src/main/java/org/springframework/security/crypto/password/Pbkdf2PasswordEncoder.java index 90023d91ec..0d5d4c72fe 100644 --- a/crypto/src/main/java/org/springframework/security/crypto/password/Pbkdf2PasswordEncoder.java +++ b/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 + * SecretKeyFactory Algorithms + * @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 + } } diff --git a/crypto/src/main/java/org/springframework/security/crypto/password/StandardPasswordEncoder.java b/crypto/src/main/java/org/springframework/security/crypto/password/StandardPasswordEncoder.java index 562f09d96b..974f618bb9 100644 --- a/crypto/src/main/java/org/springframework/security/crypto/password/StandardPasswordEncoder.java +++ b/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; diff --git a/crypto/src/test/java/org/springframework/security/crypto/codec/HexTests.java b/crypto/src/test/java/org/springframework/security/crypto/codec/HexTests.java index 8e8018308f..e464d7171e 100644 --- a/crypto/src/test/java/org/springframework/security/crypto/codec/HexTests.java +++ b/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 diff --git a/crypto/src/test/java/org/springframework/security/crypto/factory/PasswordEncoderFactoriesTests.java b/crypto/src/test/java/org/springframework/security/crypto/factory/PasswordEncoderFactoriesTests.java new file mode 100644 index 0000000000..9bf0dae34f --- /dev/null +++ b/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(); + } + +} diff --git a/crypto/src/test/java/org/springframework/security/crypto/keygen/Base64StringKeyGeneratorTests.java b/crypto/src/test/java/org/springframework/security/crypto/keygen/Base64StringKeyGeneratorTests.java new file mode 100644 index 0000000000..e199143b52 --- /dev/null +++ b/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); + } +} diff --git a/crypto/src/test/java/org/springframework/security/crypto/password/DelegatingPasswordEncoderTests.java b/crypto/src/test/java/org/springframework/security/crypto/password/DelegatingPasswordEncoderTests.java new file mode 100644 index 0000000000..04ec495d22 --- /dev/null +++ b/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 delegates; + + private DelegatingPasswordEncoder passwordEncoder; + + @Before + public void setup() { + this.delegates = new HashMap(); + 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(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); + } +} diff --git a/crypto/src/test/java/org/springframework/security/crypto/password/LdapShaPasswordEncoderTests.java b/crypto/src/test/java/org/springframework/security/crypto/password/LdapShaPasswordEncoderTests.java new file mode 100644 index 0000000000..be0f8f328c --- /dev/null +++ b/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"); + } +} diff --git a/crypto/src/test/java/org/springframework/security/crypto/password/Md4PasswordEncoderTests.java b/crypto/src/test/java/org/springframework/security/crypto/password/Md4PasswordEncoderTests.java new file mode 100644 index 0000000000..726fe36dfd --- /dev/null +++ b/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")); + } +} + diff --git a/crypto/src/test/java/org/springframework/security/crypto/password/MessageDigestPasswordEncoderTests.java b/crypto/src/test/java/org/springframework/security/crypto/password/MessageDigestPasswordEncoderTests.java new file mode 100644 index 0000000000..72a4fa36ad --- /dev/null +++ b/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; + +/** + *

+ * TestCase for Md5PasswordEncoder. + *

+ * + * @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"); + } +} diff --git a/crypto/src/test/java/org/springframework/security/crypto/password/PasswordEncoderUtilsTests.java b/crypto/src/test/java/org/springframework/security/crypto/password/PasswordEncoderUtilsTests.java new file mode 100644 index 0000000000..fcf7dbdba2 --- /dev/null +++ b/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(); + } +} diff --git a/crypto/src/test/java/org/springframework/security/crypto/password/Pbkdf2PasswordEncoderTests.java b/crypto/src/test/java/org/springframework/security/crypto/password/Pbkdf2PasswordEncoderTests.java index d641452141..125cd888f8 100644 --- a/crypto/src/test/java/org/springframework/security/crypto/password/Pbkdf2PasswordEncoderTests.java +++ b/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); } -} \ No newline at end of file +} diff --git a/crypto/src/test/resources/logback-test.xml b/crypto/src/test/resources/logback-test.xml new file mode 100644 index 0000000000..2d51ba4180 --- /dev/null +++ b/crypto/src/test/resources/logback-test.xml @@ -0,0 +1,15 @@ + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + +