1
0
mirror of synced 2026-07-19 09:35:12 +00:00

Add crypto PasswordEncoder from 5.0.x

Fixes: gh-5302
This commit is contained in:
Rob Winch
2018-05-03 16:22:50 -05:00
parent c683bc10bf
commit 0f612bf637
30 changed files with 2114 additions and 9 deletions
@@ -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
// ================================================================================================
@@ -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
// ========================================================================================================
@@ -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
@@ -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
@@ -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() {
@@ -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;
@@ -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
// ================================================================================================
@@ -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 {
/**
@@ -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() {}
}
@@ -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);
}
}
@@ -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 + "\"");
}
}
}
@@ -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);
@@ -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;
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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) {
@@ -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() {
}
}
@@ -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
}
}
@@ -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;
@@ -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
@@ -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();
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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");
}
}
@@ -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"));
}
}
@@ -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");
}
}
@@ -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();
}
}
@@ -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);
}
}
}
@@ -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>