1
0
mirror of synced 2026-08-05 17:57:15 +00:00

Add nullability to spring-security-core

Closes gh-17534
This commit is contained in:
Rob Winch
2025-07-09 08:19:59 -05:00
parent 9db1ffbd79
commit 7c887d2da1
249 changed files with 1890 additions and 841 deletions
@@ -23,7 +23,7 @@ import org.bouncycastle.crypto.params.Argon2Parameters;
import org.springframework.security.crypto.keygen.BytesKeyGenerator;
import org.springframework.security.crypto.keygen.KeyGenerators;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.password.AbstractValidatingPasswordEncoder;
/**
* <p>
@@ -44,7 +44,7 @@ import org.springframework.security.crypto.password.PasswordEncoder;
* @author Simeon Macke
* @since 5.3
*/
public class Argon2PasswordEncoder implements PasswordEncoder {
public class Argon2PasswordEncoder extends AbstractValidatingPasswordEncoder {
private static final int DEFAULT_SALT_LENGTH = 16;
@@ -108,7 +108,7 @@ public class Argon2PasswordEncoder implements PasswordEncoder {
}
@Override
public String encode(CharSequence rawPassword) {
protected String encodeNonNullPassword(String rawPassword) {
byte[] salt = this.saltGenerator.generateKey();
byte[] hash = new byte[this.hashLength];
// @formatter:off
@@ -127,11 +127,7 @@ public class Argon2PasswordEncoder implements PasswordEncoder {
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
if (encodedPassword == null) {
this.logger.warn("password hash is null");
return false;
}
protected boolean matchesNonNull(String rawPassword, String encodedPassword) {
Argon2EncodingUtils.Argon2Hash decoded;
try {
decoded = Argon2EncodingUtils.decode(encodedPassword);
@@ -148,11 +144,7 @@ public class Argon2PasswordEncoder implements PasswordEncoder {
}
@Override
public boolean upgradeEncoding(String encodedPassword) {
if (encodedPassword == null || encodedPassword.length() == 0) {
this.logger.warn("password hash is null");
return false;
}
protected boolean upgradeEncodingNonNull(String encodedPassword) {
Argon2Parameters parameters = Argon2EncodingUtils.decode(encodedPassword).getParameters();
return parameters.getMemory() < this.memory || parameters.getIterations() < this.iterations;
}
@@ -24,7 +24,7 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.password.AbstractValidatingPasswordEncoder;
/**
* Implementation of PasswordEncoder that uses the BCrypt strong hashing function. Clients
@@ -34,7 +34,7 @@ import org.springframework.security.crypto.password.PasswordEncoder;
*
* @author Dave Syer
*/
public class BCryptPasswordEncoder implements PasswordEncoder {
public class BCryptPasswordEncoder extends AbstractValidatingPasswordEncoder {
private Pattern BCRYPT_PATTERN = Pattern.compile("\\A\\$2(a|y|b)?\\$(\\d\\d)\\$[./0-9A-Za-z]{53}");
@@ -103,10 +103,7 @@ public class BCryptPasswordEncoder implements PasswordEncoder {
}
@Override
public String encode(CharSequence rawPassword) {
if (rawPassword == null) {
throw new IllegalArgumentException("rawPassword cannot be null");
}
protected String encodeNonNullPassword(String rawPassword) {
String salt = getSalt();
return BCrypt.hashpw(rawPassword.toString(), salt);
}
@@ -119,14 +116,7 @@ public class BCryptPasswordEncoder implements PasswordEncoder {
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
if (rawPassword == null) {
throw new IllegalArgumentException("rawPassword cannot be null");
}
if (encodedPassword == null || encodedPassword.length() == 0) {
this.logger.warn("Empty encoded password");
return false;
}
protected boolean matchesNonNull(String rawPassword, String encodedPassword) {
if (!this.BCRYPT_PATTERN.matcher(encodedPassword).matches()) {
this.logger.warn("Encoded password does not look like BCrypt");
return false;
@@ -135,11 +125,7 @@ public class BCryptPasswordEncoder implements PasswordEncoder {
}
@Override
public boolean upgradeEncoding(String encodedPassword) {
if (encodedPassword == null || encodedPassword.length() == 0) {
this.logger.warn("Empty encoded password");
return false;
}
protected boolean upgradeEncodingNonNull(String encodedPassword) {
Matcher matcher = this.BCRYPT_PATTERN.matcher(encodedPassword);
if (!matcher.matches()) {
throw new IllegalArgumentException("Encoded password does not look like BCrypt: " + encodedPassword);
@@ -22,8 +22,6 @@ import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import org.jspecify.annotations.Nullable;
/**
* UTF-8 Charset encoder/decoder.
* <p>
@@ -41,7 +39,10 @@ public final class Utf8 {
/**
* Get the bytes of the String in UTF-8 encoded form.
*/
public static byte[] encode(@Nullable CharSequence string) {
public static byte[] encode(CharSequence string) {
if (string == null) {
throw new IllegalArgumentException("String cannot be null");
}
try {
ByteBuffer bytes = CHARSET.newEncoder().encode(CharBuffer.wrap(string));
byte[] bytesCopy = new byte[bytes.limit()];
@@ -28,7 +28,7 @@ import org.springframework.security.crypto.util.EncodingUtils;
*
* @author Rob Worsnop
*/
public abstract class AbstractPasswordEncoder implements PasswordEncoder {
public abstract class AbstractPasswordEncoder extends AbstractValidatingPasswordEncoder {
private final BytesKeyGenerator saltGenerator;
@@ -37,29 +37,29 @@ public abstract class AbstractPasswordEncoder implements PasswordEncoder {
}
@Override
public String encode(CharSequence rawPassword) {
protected String encodeNonNullPassword(String rawPassword) {
byte[] salt = this.saltGenerator.generateKey();
byte[] encoded = encodeAndConcatenate(rawPassword, salt);
return String.valueOf(Hex.encode(encoded));
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
protected boolean matchesNonNull(String rawPassword, String encodedPassword) {
byte[] digested = Hex.decode(encodedPassword);
byte[] salt = EncodingUtils.subArray(digested, 0, this.saltGenerator.getKeyLength());
return matches(digested, encodeAndConcatenate(rawPassword, salt));
return matchesNonNull(digested, encodeAndConcatenate(rawPassword, salt));
}
protected abstract byte[] encode(CharSequence rawPassword, byte[] salt);
protected abstract byte[] encodedNonNullPassword(CharSequence rawPassword, byte[] salt);
protected byte[] encodeAndConcatenate(CharSequence rawPassword, byte[] salt) {
return EncodingUtils.concatenate(salt, encode(rawPassword, salt));
return EncodingUtils.concatenate(salt, encodedNonNullPassword(rawPassword, salt));
}
/**
* Constant time comparison to prevent against timing attacks.
*/
protected static boolean matches(byte[] expected, byte[] actual) {
protected static boolean matchesNonNull(byte[] expected, byte[] actual) {
return MessageDigest.isEqual(expected, actual);
}
@@ -0,0 +1,56 @@
/*
* Copyright 2002-2025 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
*
* https://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.jspecify.annotations.Nullable;
public abstract class AbstractValidatingPasswordEncoder implements PasswordEncoder {
@Override
public final @Nullable String encode(@Nullable CharSequence rawPassword) {
if (rawPassword == null) {
return null;
}
return encodeNonNullPassword(rawPassword.toString());
}
protected abstract String encodeNonNullPassword(String rawPassword);
@Override
public final boolean matches(@Nullable CharSequence rawPassword, @Nullable String encodedPassword) {
if (rawPassword == null || rawPassword.length() == 0 || encodedPassword == null
|| encodedPassword.length() == 0) {
return false;
}
return matchesNonNull(rawPassword.toString(), encodedPassword);
}
protected abstract boolean matchesNonNull(String rawPassword, String encodedPassword);
@Override
public final boolean upgradeEncoding(@Nullable String encodedPassword) {
if (encodedPassword == null || encodedPassword.length() == 0) {
return false;
}
return upgradeEncodingNonNull(encodedPassword);
}
protected boolean upgradeEncodingNonNull(String encodedPassword) {
return false;
}
}
@@ -125,7 +125,7 @@ import org.jspecify.annotations.Nullable;
* @since 5.0
* @see org.springframework.security.crypto.factory.PasswordEncoderFactories
*/
public class DelegatingPasswordEncoder implements PasswordEncoder {
public class DelegatingPasswordEncoder extends AbstractValidatingPasswordEncoder {
private static final String DEFAULT_ID_PREFIX = "{";
@@ -233,18 +233,12 @@ public class DelegatingPasswordEncoder implements PasswordEncoder {
}
@Override
public String encode(CharSequence rawPassword) {
if (rawPassword == null) {
throw new IllegalArgumentException("rawPassword cannot be null");
}
protected String encodeNonNullPassword(String rawPassword) {
return this.idPrefix + this.idForEncode + this.idSuffix + this.passwordEncoderForEncode.encode(rawPassword);
}
@Override
public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {
if (rawPassword == null && prefixEncodedPassword == null) {
return true;
}
protected boolean matchesNonNull(String rawPassword, String prefixEncodedPassword) {
String id = extractId(prefixEncodedPassword);
PasswordEncoder delegate = this.idToPasswordEncoder.get(id);
if (delegate == null) {
@@ -270,10 +264,7 @@ public class DelegatingPasswordEncoder implements PasswordEncoder {
}
@Override
public boolean upgradeEncoding(@Nullable String prefixEncodedPassword) {
if (prefixEncodedPassword == null) {
return false;
}
protected boolean upgradeEncodingNonNull(String prefixEncodedPassword) {
String id = extractId(prefixEncodedPassword);
if (!this.idForEncode.equalsIgnoreCase(id)) {
return true;
@@ -293,15 +284,15 @@ public class DelegatingPasswordEncoder implements PasswordEncoder {
* Default {@link PasswordEncoder} that throws an exception telling that a suitable
* {@link PasswordEncoder} for the id could not be found.
*/
private class UnmappedIdPasswordEncoder implements PasswordEncoder {
private class UnmappedIdPasswordEncoder extends AbstractValidatingPasswordEncoder {
@Override
public String encode(CharSequence rawPassword) {
protected String encodeNonNullPassword(String rawPassword) {
throw new UnsupportedOperationException("encode is not supported");
}
@Override
public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {
protected boolean matchesNonNull(String rawPassword, String prefixEncodedPassword) {
String id = extractId(prefixEncodedPassword);
if (id != null && !id.isBlank()) {
throw new IllegalArgumentException(String.format(NO_PASSWORD_ENCODER_MAPPED, id));
@@ -46,7 +46,7 @@ import org.springframework.security.crypto.keygen.KeyGenerators;
* indicate that this is a legacy implementation and using it is considered insecure.
*/
@Deprecated
public class LdapShaPasswordEncoder implements PasswordEncoder {
public class LdapShaPasswordEncoder extends AbstractValidatingPasswordEncoder {
/** The number of bytes in a SHA hash */
private static final int SHA_LENGTH = 20;
@@ -88,17 +88,17 @@ public class LdapShaPasswordEncoder implements PasswordEncoder {
* 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.
* @param rawPassword the password to be encoded.
* @return the encoded password in the specified format
*
*/
@Override
public String encode(CharSequence rawPass) {
protected String encodeNonNullPassword(String rawPassword) {
byte[] salt = this.saltGenerator.generateKey();
return encode(rawPass, salt);
return encode(rawPassword, salt);
}
private String encode(@Nullable CharSequence rawPassword, byte @Nullable [] salt) {
private String encode(CharSequence rawPassword, byte @Nullable [] salt) {
MessageDigest sha = getSha(rawPassword);
if (salt != null) {
sha.update(salt);
@@ -108,7 +108,7 @@ public class LdapShaPasswordEncoder implements PasswordEncoder {
return prefix + Utf8.decode(Base64.getEncoder().encode(hash));
}
private MessageDigest getSha(@Nullable CharSequence rawPassword) {
private MessageDigest getSha(CharSequence rawPassword) {
try {
MessageDigest sha = MessageDigest.getInstance("SHA");
sha.update(Utf8.encode(rawPassword));
@@ -143,11 +143,7 @@ public class LdapShaPasswordEncoder implements PasswordEncoder {
* @return true if they match (independent of the case of the prefix).
*/
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return matches((rawPassword != null) ? rawPassword.toString() : null, encodedPassword);
}
private boolean matches(@Nullable String rawPassword, String encodedPassword) {
protected boolean matchesNonNull(String rawPassword, String encodedPassword) {
String prefix = extractPrefix(encodedPassword);
if (prefix == null) {
return PasswordEncoderUtils.equals(encodedPassword, rawPassword);
@@ -78,7 +78,7 @@ import org.springframework.security.crypto.keygen.StringKeyGenerator;
* indicate that this is a legacy implementation and using it is considered insecure.
*/
@Deprecated
public class Md4PasswordEncoder implements PasswordEncoder {
public class Md4PasswordEncoder extends AbstractValidatingPasswordEncoder {
private static final String PREFIX = "{";
@@ -100,7 +100,7 @@ public class Md4PasswordEncoder implements PasswordEncoder {
* encodeHashAsBase64 is enabled.
*/
@Override
public String encode(CharSequence rawPassword) {
public String encodeNonNullPassword(String rawPassword) {
String salt = PREFIX + this.saltGenerator.generateKey() + SUFFIX;
return digest(salt, rawPassword);
}
@@ -114,11 +114,11 @@ public class Md4PasswordEncoder implements PasswordEncoder {
Md4 md4 = new Md4();
md4.update(saltedPasswordBytes, 0, saltedPasswordBytes.length);
byte[] digest = md4.digest();
String encoded = encode(digest);
String encoded = encodedNonNullPassword(digest);
return salt + encoded;
}
private String encode(byte[] digest) {
private String encodedNonNullPassword(byte[] digest) {
if (this.encodeHashAsBase64) {
return Utf8.decode(Base64.getEncoder().encode(digest));
}
@@ -133,7 +133,7 @@ public class Md4PasswordEncoder implements PasswordEncoder {
* @return true or false
*/
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
protected boolean matchesNonNull(String rawPassword, String encodedPassword) {
String salt = extractSalt(encodedPassword);
String rawPasswordEncoded = digest(salt, rawPassword);
return PasswordEncoderUtils.equals(encodedPassword.toString(), rawPasswordEncoded);
@@ -82,7 +82,7 @@ import org.springframework.security.crypto.keygen.StringKeyGenerator;
* indicate that this is a legacy implementation and using it is considered insecure.
*/
@Deprecated
public class MessageDigestPasswordEncoder implements PasswordEncoder {
public class MessageDigestPasswordEncoder extends AbstractValidatingPasswordEncoder {
private static final String PREFIX = "{";
@@ -116,7 +116,7 @@ public class MessageDigestPasswordEncoder implements PasswordEncoder {
* encodeHashAsBase64 is enabled.
*/
@Override
public String encode(CharSequence rawPassword) {
protected String encodeNonNullPassword(String rawPassword) {
String salt = PREFIX + this.saltGenerator.generateKey() + SUFFIX;
return digest(salt, rawPassword);
}
@@ -124,11 +124,11 @@ public class MessageDigestPasswordEncoder implements PasswordEncoder {
private String digest(String salt, CharSequence rawPassword) {
String saltedPassword = rawPassword + salt;
byte[] digest = this.digester.digest(Utf8.encode(saltedPassword));
String encoded = encode(digest);
String encoded = encodedNonNullPassword(digest);
return salt + encoded;
}
private String encode(byte[] digest) {
private String encodedNonNullPassword(byte[] digest) {
if (this.encodeHashAsBase64) {
return Utf8.decode(Base64.getEncoder().encode(digest));
}
@@ -143,7 +143,7 @@ public class MessageDigestPasswordEncoder implements PasswordEncoder {
* @return true or false
*/
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
protected boolean matchesNonNull(String rawPassword, String encodedPassword) {
String salt = extractSalt(encodedPassword);
String rawPasswordEncoded = digest(salt, rawPassword);
return PasswordEncoderUtils.equals(encodedPassword.toString(), rawPasswordEncoded);
@@ -31,7 +31,7 @@ package org.springframework.security.crypto.password;
* legacy implementation and using it is considered insecure.
*/
@Deprecated
public final class NoOpPasswordEncoder implements PasswordEncoder {
public final class NoOpPasswordEncoder extends AbstractValidatingPasswordEncoder {
private static final PasswordEncoder INSTANCE = new NoOpPasswordEncoder();
@@ -39,12 +39,12 @@ public final class NoOpPasswordEncoder implements PasswordEncoder {
}
@Override
public String encode(CharSequence rawPassword) {
protected String encodeNonNullPassword(String rawPassword) {
return rawPassword.toString();
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
protected boolean matchesNonNull(String rawPassword, String encodedPassword) {
return rawPassword.toString().equals(encodedPassword);
}
@@ -16,40 +16,50 @@
package org.springframework.security.crypto.password;
import org.jspecify.annotations.Nullable;
/**
* Service interface for encoding passwords.
*
* The preferred implementation is {@code BCryptPasswordEncoder}.
*
* @author Keith Donald
* @author Rob Winch
*/
public interface PasswordEncoder {
/**
* Encode the raw password. Generally, a good encoding algorithm applies a SHA-1 or
* greater hash combined with an 8-byte or greater randomly generated salt.
* Encode the raw password. Generally, a good encoding algorithm uses an adaptive one
* way function.
* @param rawPassword a password that has not been encoded. The value can be null in
* the event that the user has no password; in which case the result must be null.
* @return A non-null encoded password, unless the rawPassword was null in which case
* the result must be null.
*/
String encode(CharSequence rawPassword);
@Nullable String encode(@Nullable CharSequence rawPassword);
/**
* Verify the encoded password obtained from storage matches the submitted raw
* password after it too is encoded. Returns true if the passwords match, false if
* they do not. The stored password itself is never decoded.
* @param rawPassword the raw password to encode and match
* @param encodedPassword the encoded password from storage to compare with
* they do not. The stored password itself is never decoded. Never true if either
* rawPassword or encodedPassword is null or an empty String.
* @param rawPassword the raw password to encode and match.
* @param encodedPassword the encoded password from storage to compare with.
* @return true if the raw password, after encoding, matches the encoded password from
* storage
* storage.
*/
boolean matches(CharSequence rawPassword, String encodedPassword);
boolean matches(@Nullable CharSequence rawPassword, @Nullable String encodedPassword);
/**
* Returns true if the encoded password should be encoded again for better security,
* else false. The default implementation always returns false.
* @param encodedPassword the encoded password to check
* @param encodedPassword the encoded password to check. Possibly null if the user did
* not have a password.
* @return true if the encoded password should be encoded again for better security,
* else false.
* else false. If encodedPassword is null (the user didn't have a password), then
* always false.
*/
default boolean upgradeEncoding(String encodedPassword) {
default boolean upgradeEncoding(@Nullable String encodedPassword) {
return false;
}
@@ -46,7 +46,7 @@ import org.springframework.security.crypto.util.EncodingUtils;
* @author Loïc Guibert
* @since 4.1
*/
public class Pbkdf2PasswordEncoder implements PasswordEncoder {
public class Pbkdf2PasswordEncoder extends AbstractValidatingPasswordEncoder {
private static final int DEFAULT_SALT_LENGTH = 16;
@@ -194,13 +194,13 @@ public class Pbkdf2PasswordEncoder implements PasswordEncoder {
}
@Override
public String encode(CharSequence rawPassword) {
protected String encodeNonNullPassword(String rawPassword) {
byte[] salt = this.saltGenerator.generateKey();
byte[] encoded = encode(rawPassword, salt);
return encode(encoded);
byte[] encoded = encodedNonNullPassword(rawPassword, salt);
return encodedNonNullPassword(encoded);
}
private String encode(byte[] bytes) {
private String encodedNonNullPassword(byte[] bytes) {
if (this.encodeHashAsBase64) {
return Base64.getEncoder().encodeToString(bytes);
}
@@ -208,10 +208,10 @@ public class Pbkdf2PasswordEncoder implements PasswordEncoder {
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
protected boolean matchesNonNull(String rawPassword, String encodedPassword) {
byte[] digested = decode(encodedPassword);
byte[] salt = EncodingUtils.subArray(digested, 0, this.saltGenerator.getKeyLength());
return MessageDigest.isEqual(digested, encode(rawPassword, salt));
return MessageDigest.isEqual(digested, encodedNonNullPassword(rawPassword, salt));
}
private byte[] decode(String encodedBytes) {
@@ -221,7 +221,7 @@ public class Pbkdf2PasswordEncoder implements PasswordEncoder {
return Hex.decode(encodedBytes);
}
private byte[] encode(CharSequence rawPassword, byte[] salt) {
private byte[] encodedNonNullPassword(CharSequence rawPassword, byte[] salt) {
try {
PBEKeySpec spec = new PBEKeySpec(rawPassword.toString().toCharArray(),
EncodingUtils.concatenate(salt, this.secret), this.iterations, this.hashWidth);
@@ -48,7 +48,7 @@ import org.springframework.security.crypto.util.EncodingUtils;
* indicate that this is a legacy implementation and using it is considered insecure.
*/
@Deprecated
public final class StandardPasswordEncoder implements PasswordEncoder {
public final class StandardPasswordEncoder extends AbstractValidatingPasswordEncoder {
private static final int DEFAULT_ITERATIONS = 1024;
@@ -75,12 +75,12 @@ public final class StandardPasswordEncoder implements PasswordEncoder {
}
@Override
public String encode(CharSequence rawPassword) {
return encode(rawPassword, this.saltGenerator.generateKey());
protected String encodeNonNullPassword(String rawPassword) {
return encodedNonNullPassword(rawPassword, this.saltGenerator.generateKey());
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
protected boolean matchesNonNull(String rawPassword, String encodedPassword) {
byte[] digested = decode(encodedPassword);
byte[] salt = EncodingUtils.subArray(digested, 0, this.saltGenerator.getKeyLength());
return MessageDigest.isEqual(digested, digest(rawPassword, salt));
@@ -92,7 +92,7 @@ public final class StandardPasswordEncoder implements PasswordEncoder {
this.saltGenerator = KeyGenerators.secureRandom();
}
private String encode(CharSequence rawPassword, byte[] salt) {
private String encodedNonNullPassword(CharSequence rawPassword, byte[] salt) {
byte[] digest = digest(rawPassword, salt);
return new String(Hex.encode(digest));
}
@@ -26,7 +26,7 @@ import org.bouncycastle.crypto.generators.SCrypt;
import org.springframework.security.crypto.codec.Utf8;
import org.springframework.security.crypto.keygen.BytesKeyGenerator;
import org.springframework.security.crypto.keygen.KeyGenerators;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.password.AbstractValidatingPasswordEncoder;
/**
* <p>
@@ -56,7 +56,7 @@ import org.springframework.security.crypto.password.PasswordEncoder;
* @author Rob Winch
*
*/
public class SCryptPasswordEncoder implements PasswordEncoder {
public class SCryptPasswordEncoder extends AbstractValidatingPasswordEncoder {
private static final int DEFAULT_CPU_COST = 65536;
@@ -146,24 +146,17 @@ public class SCryptPasswordEncoder implements PasswordEncoder {
}
@Override
public String encode(CharSequence rawPassword) {
protected String encodeNonNullPassword(String rawPassword) {
return digest(rawPassword, this.saltGenerator.generateKey());
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
if (encodedPassword == null || encodedPassword.length() < this.keyLength) {
this.logger.warn("Empty encoded password");
return false;
}
protected boolean matchesNonNull(String rawPassword, String encodedPassword) {
return decodeAndCheckMatches(rawPassword, encodedPassword);
}
@Override
public boolean upgradeEncoding(String encodedPassword) {
if (encodedPassword == null || encodedPassword.isEmpty()) {
return false;
}
protected boolean upgradeEncodingNonNull(String encodedPassword) {
String[] parts = encodedPassword.split("\\$");
if (parts.length != 4) {
throw new IllegalArgumentException("Encoded password does not look like SCrypt: " + encodedPassword);
@@ -19,6 +19,7 @@ package org.springframework.security.crypto.argon2;
import java.lang.reflect.Field;
import java.util.Arrays;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
@@ -26,6 +27,7 @@ import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.crypto.keygen.BytesKeyGenerator;
import org.springframework.security.crypto.password.AbstractPasswordEncoderValidationTests;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
@@ -34,56 +36,59 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
* @author Simeon Macke
*/
@ExtendWith(MockitoExtension.class)
public class Argon2PasswordEncoderTests {
public class Argon2PasswordEncoderTests extends AbstractPasswordEncoderValidationTests {
@Mock
private BytesKeyGenerator keyGeneratorMock;
private Argon2PasswordEncoder encoder = Argon2PasswordEncoder.defaultsForSpringSecurity_v5_2();
@BeforeEach
void setup() {
setEncoder(Argon2PasswordEncoder.defaultsForSpringSecurity_v5_2());
}
@Test
public void encodeDoesNotEqualPassword() {
String result = this.encoder.encode("password");
public void encodedNonNullPasswordDoesNotEqualPassword() {
String result = getEncoder().encode("password");
assertThat(result).isNotEqualTo("password");
}
@Test
public void encodeWhenEqualPasswordThenMatches() {
String result = this.encoder.encode("password");
assertThat(this.encoder.matches("password", result)).isTrue();
public void encodedNonNullPasswordWhenEqualPasswordThenMatches() {
String result = getEncoder().encode("password");
assertThat(getEncoder().matches("password", result)).isTrue();
}
@Test
public void encodeWhenEqualWithUnicodeThenMatches() {
String result = this.encoder.encode("passw\u9292rd");
assertThat(this.encoder.matches("pass\u9292\u9292rd", result)).isFalse();
assertThat(this.encoder.matches("passw\u9292rd", result)).isTrue();
public void encodedNonNullPasswordWhenEqualWithUnicodeThenMatches() {
String result = getEncoder().encode("passw\u9292rd");
assertThat(getEncoder().matches("pass\u9292\u9292rd", result)).isFalse();
assertThat(getEncoder().matches("passw\u9292rd", result)).isTrue();
}
@Test
public void encodeWhenNotEqualThenNotMatches() {
String result = this.encoder.encode("password");
assertThat(this.encoder.matches("bogus", result)).isFalse();
public void encodedNonNullPasswordWhenNotEqualThenNotMatches() {
String result = getEncoder().encode("password");
assertThat(getEncoder().matches("bogus", result)).isFalse();
}
@Test
public void encodeWhenEqualPasswordWithCustomParamsThenMatches() {
this.encoder = new Argon2PasswordEncoder(20, 64, 4, 256, 4);
String result = this.encoder.encode("password");
assertThat(this.encoder.matches("password", result)).isTrue();
public void encodedNonNullPasswordWhenEqualPasswordWithCustomParamsThenMatches() {
setEncoder(new Argon2PasswordEncoder(20, 64, 4, 256, 4));
String result = getEncoder().encode("password");
assertThat(getEncoder().matches("password", result)).isTrue();
}
@Test
public void encodeWhenRanTwiceThenResultsNotEqual() {
public void encodedNonNullPasswordWhenRanTwiceThenResultsNotEqual() {
String password = "secret";
assertThat(this.encoder.encode(password)).isNotEqualTo(this.encoder.encode(password));
assertThat(getEncoder().encode(password)).isNotEqualTo(getEncoder().encode(password));
}
@Test
public void encodeWhenRanTwiceWithCustomParamsThenNotEquals() {
this.encoder = new Argon2PasswordEncoder(20, 64, 4, 256, 4);
public void encodedNonNullPasswordWhenRanTwiceWithCustomParamsThenNotEquals() {
setEncoder(new Argon2PasswordEncoder(20, 64, 4, 256, 4));
String password = "secret";
assertThat(this.encoder.encode(password)).isNotEqualTo(this.encoder.encode(password));
assertThat(getEncoder().encode(password)).isNotEqualTo(getEncoder().encode(password));
}
@Test
@@ -97,55 +102,56 @@ public class Argon2PasswordEncoderTests {
@Test
public void matchesWhenEncodedPassIsNullThenFalse() {
assertThat(this.encoder.matches("password", null)).isFalse();
assertThat(getEncoder().matches("password", null)).isFalse();
}
@Test
public void matchesWhenEncodedPassIsEmptyThenFalse() {
assertThat(this.encoder.matches("password", "")).isFalse();
assertThat(getEncoder().matches("password", "")).isFalse();
}
@Test
public void matchesWhenEncodedPassIsBogusThenFalse() {
assertThat(this.encoder.matches("password", "012345678901234567890123456789")).isFalse();
assertThat(getEncoder().matches("password", "012345678901234567890123456789")).isFalse();
}
@Test
public void encodeWhenUsingPredictableSaltThenEqualTestHash() throws Exception {
public void encodedNonNullPasswordWhenUsingPredictableSaltThenEqualTestHash() throws Exception {
injectPredictableSaltGen();
String hash = this.encoder.encode("sometestpassword");
String hash = getEncoder().encode("sometestpassword");
assertThat(hash).isEqualTo(
"$argon2id$v=19$m=4096,t=3,p=1$QUFBQUFBQUFBQUFBQUFBQQ$hmmTNyJlwbb6HAvFoHFWF+u03fdb0F2qA+39oPlcAqo");
}
@Test
public void encodeWhenUsingPredictableSaltWithCustomParamsThenEqualTestHash() throws Exception {
this.encoder = new Argon2PasswordEncoder(16, 32, 4, 512, 5);
public void encodedNonNullPasswordWhenUsingPredictableSaltWithCustomParamsThenEqualTestHash() throws Exception {
setEncoder(new Argon2PasswordEncoder(16, 32, 4, 512, 5));
injectPredictableSaltGen();
String hash = this.encoder.encode("sometestpassword");
String hash = getEncoder().encode("sometestpassword");
assertThat(hash).isEqualTo(
"$argon2id$v=19$m=512,t=5,p=4$QUFBQUFBQUFBQUFBQUFBQQ$PNv4C3K50bz3rmON+LtFpdisD7ePieLNq+l5iUHgc1k");
}
@Test
public void encodeWhenUsingPredictableSaltWithDefaultsForSpringSecurity_v5_8ThenEqualTestHash() throws Exception {
this.encoder = Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8();
public void encodedNonNullPasswordWhenUsingPredictableSaltWithDefaultsForSpringSecurity_v5_8ThenEqualTestHash()
throws Exception {
setEncoder(Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8());
injectPredictableSaltGen();
String hash = this.encoder.encode("sometestpassword");
String hash = getEncoder().encode("sometestpassword");
assertThat(hash).isEqualTo(
"$argon2id$v=19$m=16384,t=2,p=1$QUFBQUFBQUFBQUFBQUFBQQ$zGt5MiNPSUOo4/7jBcJMayCPfcsLJ4c0WUxhwGDIYPw");
}
@Test
public void upgradeEncodingWhenSameEncodingThenFalse() {
String hash = this.encoder.encode("password");
assertThat(this.encoder.upgradeEncoding(hash)).isFalse();
String hash = getEncoder().encode("password");
assertThat(getEncoder().upgradeEncoding(hash)).isFalse();
}
@Test
public void upgradeEncodingWhenSameStandardParamsThenFalse() {
Argon2PasswordEncoder newEncoder = Argon2PasswordEncoder.defaultsForSpringSecurity_v5_2();
String hash = this.encoder.encode("password");
String hash = getEncoder().encode("password");
assertThat(newEncoder.upgradeEncoding(hash)).isFalse();
}
@@ -183,17 +189,17 @@ public class Argon2PasswordEncoderTests {
@Test
public void upgradeEncodingWhenEncodedPassIsNullThenFalse() {
assertThat(this.encoder.upgradeEncoding(null)).isFalse();
assertThat(getEncoder().upgradeEncoding(null)).isFalse();
}
@Test
public void upgradeEncodingWhenEncodedPassIsEmptyThenFalse() {
assertThat(this.encoder.upgradeEncoding("")).isFalse();
assertThat(getEncoder().upgradeEncoding("")).isFalse();
}
@Test
public void upgradeEncodingWhenEncodedPassIsBogusThenThrowException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.encoder.upgradeEncoding("thisIsNoValidHash"));
assertThatIllegalArgumentException().isThrownBy(() -> getEncoder().upgradeEncoding("thisIsNoValidHash"));
}
private void injectPredictableSaltGen() throws Exception {
@@ -203,9 +209,9 @@ public class Argon2PasswordEncoderTests {
// we can't use the @InjectMock-annotation because the salt-generator is set in
// the constructor
// and Mockito will only inject mocks if they are null
Field saltGen = this.encoder.getClass().getDeclaredField("saltGenerator");
Field saltGen = getEncoder().getClass().getDeclaredField("saltGenerator");
saltGen.setAccessible(true);
saltGen.set(this.encoder, this.keyGeneratorMock);
saltGen.set(getEncoder(), this.keyGeneratorMock);
saltGen.setAccessible(false);
}
@@ -18,8 +18,11 @@ package org.springframework.security.crypto.bcrypt;
import java.security.SecureRandom;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.security.crypto.password.AbstractPasswordEncoderValidationTests;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
@@ -27,107 +30,107 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
* @author Dave Syer
*
*/
public class BCryptPasswordEncoderTests {
public class BCryptPasswordEncoderTests extends AbstractPasswordEncoderValidationTests {
@BeforeEach
void setup() {
setEncoder(new BCryptPasswordEncoder());
}
@Test
// gh-5548
public void emptyRawPasswordDoesNotMatchPassword() {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
String result = encoder.encode("password");
assertThat(encoder.matches("", result)).isFalse();
String result = getEncoder().encode("password");
assertThat(getEncoder().matches("", result)).isFalse();
}
@Test
public void $2yMatches() {
// $2y is default version
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
String result = encoder.encode("password");
String result = getEncoder().encode("password");
assertThat(result.equals("password")).isFalse();
assertThat(encoder.matches("password", result)).isTrue();
assertThat(getEncoder().matches("password", result)).isTrue();
}
@Test
public void $2aMatches() {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(BCryptPasswordEncoder.BCryptVersion.$2A);
String result = encoder.encode("password");
String result = getEncoder().encode("password");
assertThat(result.equals("password")).isFalse();
assertThat(encoder.matches("password", result)).isTrue();
assertThat(getEncoder().matches("password", result)).isTrue();
}
@Test
public void $2bMatches() {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(BCryptPasswordEncoder.BCryptVersion.$2B);
String result = encoder.encode("password");
setEncoder(new BCryptPasswordEncoder(BCryptPasswordEncoder.BCryptVersion.$2B));
String result = getEncoder().encode("password");
assertThat(result.equals("password")).isFalse();
assertThat(encoder.matches("password", result)).isTrue();
assertThat(getEncoder().matches("password", result)).isTrue();
}
@Test
public void $2yUnicode() {
// $2y is default version
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
String result = encoder.encode("passw\u9292rd");
assertThat(encoder.matches("pass\u9292\u9292rd", result)).isFalse();
assertThat(encoder.matches("passw\u9292rd", result)).isTrue();
String result = getEncoder().encode("passw\u9292rd");
assertThat(getEncoder().matches("pass\u9292\u9292rd", result)).isFalse();
assertThat(getEncoder().matches("passw\u9292rd", result)).isTrue();
}
@Test
public void $2aUnicode() {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(BCryptPasswordEncoder.BCryptVersion.$2A);
String result = encoder.encode("passw\u9292rd");
assertThat(encoder.matches("pass\u9292\u9292rd", result)).isFalse();
assertThat(encoder.matches("passw\u9292rd", result)).isTrue();
setEncoder(new BCryptPasswordEncoder(BCryptPasswordEncoder.BCryptVersion.$2A));
String result = getEncoder().encode("passw\u9292rd");
assertThat(getEncoder().matches("pass\u9292\u9292rd", result)).isFalse();
assertThat(getEncoder().matches("passw\u9292rd", result)).isTrue();
}
@Test
public void $2bUnicode() {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(BCryptPasswordEncoder.BCryptVersion.$2B);
String result = encoder.encode("passw\u9292rd");
assertThat(encoder.matches("pass\u9292\u9292rd", result)).isFalse();
assertThat(encoder.matches("passw\u9292rd", result)).isTrue();
setEncoder(new BCryptPasswordEncoder(BCryptPasswordEncoder.BCryptVersion.$2B));
String result = getEncoder().encode("passw\u9292rd");
assertThat(getEncoder().matches("pass\u9292\u9292rd", result)).isFalse();
assertThat(getEncoder().matches("passw\u9292rd", result)).isTrue();
}
@Test
public void $2yNotMatches() {
// $2y is default version
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
String result = encoder.encode("password");
assertThat(encoder.matches("bogus", result)).isFalse();
String result = getEncoder().encode("password");
assertThat(getEncoder().matches("bogus", result)).isFalse();
}
@Test
public void $2aNotMatches() {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(BCryptPasswordEncoder.BCryptVersion.$2A);
String result = encoder.encode("password");
assertThat(encoder.matches("bogus", result)).isFalse();
setEncoder(new BCryptPasswordEncoder(BCryptPasswordEncoder.BCryptVersion.$2A));
String result = getEncoder().encode("password");
assertThat(getEncoder().matches("bogus", result)).isFalse();
}
@Test
public void $2bNotMatches() {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(BCryptPasswordEncoder.BCryptVersion.$2B);
String result = encoder.encode("password");
assertThat(encoder.matches("bogus", result)).isFalse();
setEncoder(new BCryptPasswordEncoder(BCryptPasswordEncoder.BCryptVersion.$2B));
String result = getEncoder().encode("password");
assertThat(getEncoder().matches("bogus", result)).isFalse();
}
@Test
public void $2yCustomStrength() {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(8);
String result = encoder.encode("password");
assertThat(encoder.matches("password", result)).isTrue();
setEncoder(new BCryptPasswordEncoder(8));
String result = getEncoder().encode("password");
assertThat(getEncoder().matches("password", result)).isTrue();
}
@Test
public void $2aCustomStrength() {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(BCryptPasswordEncoder.BCryptVersion.$2A, 8);
String result = encoder.encode("password");
assertThat(encoder.matches("password", result)).isTrue();
setEncoder(new BCryptPasswordEncoder(BCryptPasswordEncoder.BCryptVersion.$2A, 8));
String result = getEncoder().encode("password");
assertThat(getEncoder().matches("password", result)).isTrue();
}
@Test
public void $2bCustomStrength() {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(BCryptPasswordEncoder.BCryptVersion.$2B, 8);
String result = encoder.encode("password");
assertThat(encoder.matches("password", result)).isTrue();
setEncoder(new BCryptPasswordEncoder(BCryptPasswordEncoder.BCryptVersion.$2B, 8));
String result = getEncoder().encode("password");
assertThat(getEncoder().matches("password", result)).isTrue();
}
@Test
@@ -142,27 +145,25 @@ public class BCryptPasswordEncoderTests {
@Test
public void customRandom() {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(8, new SecureRandom());
String result = encoder.encode("password");
assertThat(encoder.matches("password", result)).isTrue();
setEncoder(new BCryptPasswordEncoder(8, new SecureRandom()));
String result = getEncoder().encode("password");
assertThat(getEncoder().matches("password", result)).isTrue();
}
@Test
public void doesntMatchNullEncodedValue() {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
assertThat(encoder.matches("password", null)).isFalse();
setEncoder(new BCryptPasswordEncoder());
assertThat(getEncoder().matches("password", null)).isFalse();
}
@Test
public void doesntMatchEmptyEncodedValue() {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
assertThat(encoder.matches("password", "")).isFalse();
assertThat(getEncoder().matches("password", "")).isFalse();
}
@Test
public void doesntMatchBogusEncodedValue() {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
assertThat(encoder.matches("password", "012345678901234567890123456789")).isFalse();
assertThat(getEncoder().matches("password", "012345678901234567890123456789")).isFalse();
}
@Test
@@ -181,9 +182,8 @@ public class BCryptPasswordEncoderTests {
*/
@Test
public void upgradeFromNullOrEmpty() {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
assertThat(encoder.upgradeEncoding(null)).isFalse();
assertThat(encoder.upgradeEncoding("")).isFalse();
assertThat(getEncoder().upgradeEncoding(null)).isFalse();
assertThat(getEncoder().upgradeEncoding("")).isFalse();
}
/**
@@ -192,65 +192,48 @@ public class BCryptPasswordEncoderTests {
*/
@Test
public void upgradeFromNonBCrypt() {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
assertThatIllegalArgumentException().isThrownBy(() -> encoder.upgradeEncoding("not-a-bcrypt-password"));
}
@Test
public void encodeNullRawPassword() {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
assertThatIllegalArgumentException().isThrownBy(() -> encoder.encode(null));
}
@Test
public void matchNullRawPassword() {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
assertThatIllegalArgumentException().isThrownBy(() -> encoder.matches(null, "does-not-matter"));
assertThatIllegalArgumentException().isThrownBy(() -> getEncoder().upgradeEncoding("not-a-bcrypt-password"));
}
@Test
public void upgradeWhenNoRoundsThenTrue() {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
assertThat(encoder.upgradeEncoding("$2a$00$9N8N35BVs5TLqGL3pspAte5OWWA2a2aZIs.EGp7At7txYakFERMue")).isTrue();
assertThat(getEncoder().upgradeEncoding("$2a$00$9N8N35BVs5TLqGL3pspAte5OWWA2a2aZIs.EGp7At7txYakFERMue"))
.isTrue();
}
@Test
public void checkWhenNoRoundsThenTrue() {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
assertThat(encoder.matches("password", "$2a$00$9N8N35BVs5TLqGL3pspAte5OWWA2a2aZIs.EGp7At7txYakFERMue"))
assertThat(getEncoder().matches("password", "$2a$00$9N8N35BVs5TLqGL3pspAte5OWWA2a2aZIs.EGp7At7txYakFERMue"))
.isTrue();
assertThat(encoder.matches("wrong", "$2a$00$9N8N35BVs5TLqGL3pspAte5OWWA2a2aZIs.EGp7At7txYakFERMue")).isFalse();
assertThat(getEncoder().matches("wrong", "$2a$00$9N8N35BVs5TLqGL3pspAte5OWWA2a2aZIs.EGp7At7txYakFERMue"))
.isFalse();
}
@Test
public void encodeWhenPasswordOverMaxLengthThenThrowIllegalArgumentException() {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
String password72chars = "123456789012345678901234567890123456789012345678901234567890123456789012";
encoder.encode(password72chars);
getEncoder().encode(password72chars);
String password73chars = password72chars + "3";
assertThatIllegalArgumentException().isThrownBy(() -> encoder.encode(password73chars));
assertThatIllegalArgumentException().isThrownBy(() -> getEncoder().encode(password73chars));
}
@Test
public void matchesWhenPasswordOverMaxLengthThenAllowToMatch() {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
String password71chars = "12345678901234567890123456789012345678901234567890123456789012345678901";
String encodedPassword71chars = "$2a$10$jx3x2FaF.iX5QZ9i3O424Os2Ou5P5JrnedmWYHuDyX8JKA4Unp4xq";
assertThat(encoder.matches(password71chars, encodedPassword71chars)).isTrue();
assertThat(getEncoder().matches(password71chars, encodedPassword71chars)).isTrue();
String password72chars = password71chars + "2";
String encodedPassword72chars = "$2a$10$oXYO6/UvbsH5rQEraBkl6uheccBqdB3n.RaWbrimog9hS2GX4lo/O";
assertThat(encoder.matches(password72chars, encodedPassword72chars)).isTrue();
assertThat(getEncoder().matches(password72chars, encodedPassword72chars)).isTrue();
// Max length is 72 bytes, however, we need to ensure backwards compatibility
// for previously encoded passwords that are greater than 72 bytes and allow the
// match to be performed.
String password73chars = password72chars + "3";
String encodedPassword73chars = "$2a$10$1l9.kvQTsqNLiCYFqmKtQOHkp.BrgIrwsnTzWo9jdbQRbuBYQ/AVK";
assertThat(encoder.matches(password73chars, encodedPassword73chars)).isTrue();
assertThat(getEncoder().matches(password73chars, encodedPassword73chars)).isTrue();
}
}
@@ -0,0 +1,40 @@
/*
* Copyright 2002-2025 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
*
* https://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.jupiter.api.BeforeEach;
/**
* Test {@link AbstractPasswordEncoder} (not intended to be extended).
*
* @author Rob Winch
* @see AbstractPasswordEncoderValidationTests
*/
final class AbstractPasswordEncoderTests extends AbstractPasswordEncoderValidationTests {
@BeforeEach
void setup() {
setEncoder(new AbstractPasswordEncoder() {
@Override
protected byte[] encodedNonNullPassword(CharSequence rawPassword, byte[] salt) {
return new byte[0];
}
});
}
}
@@ -0,0 +1,70 @@
/*
* Copyright 2002-2025 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
*
* https://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.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* A base class for other tests to perform validation of the arguments to
* {@link PasswordEncoder} instances in a consistent way.
*
* @author Rob Winch
*/
public abstract class AbstractPasswordEncoderValidationTests {
private PasswordEncoder encoder;
protected void setEncoder(PasswordEncoder encoder) {
this.encoder = encoder;
}
protected <T extends PasswordEncoder> T getEncoder(Class<T> clazz) {
return getEncoder();
}
protected <T extends PasswordEncoder> T getEncoder() {
return (T) this.encoder;
}
@Test
void encodeWhenNullThenNull() {
assertThat(this.encoder.encode(null)).isNull();
}
@Test
void matchesWhenEncodedPasswordNullThenFalse() {
assertThat(this.encoder.matches("raw", null)).isFalse();
}
@Test
void matchesWhenEncodedPasswordEmptyThenFalse() {
assertThat(this.encoder.matches("raw", "")).isFalse();
}
@Test
void matchesWhenRawPasswordNullThenFalse() {
assertThat(this.encoder.matches(null, this.encoder.encode("password"))).isFalse();
}
@Test
void matchesWhenRawPasswordEmptyThenFalse() {
assertThat(this.encoder.matches("", this.encoder.encode("password"))).isFalse();
}
}
@@ -0,0 +1,43 @@
/*
* Copyright 2002-2025 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
*
* https://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.jupiter.api.BeforeEach;
/**
* Test {@link AbstractValidatingPasswordEncoder}.
*
* @author Rob Winch
*/
class AbstractValidatingPasswordEncoderTests extends AbstractPasswordEncoderValidationTests {
@BeforeEach
void setup() {
setEncoder(new AbstractValidatingPasswordEncoder() {
@Override
protected String encodeNonNullPassword(String rawPassword) {
return "";
}
@Override
protected boolean matchesNonNull(String rawPassword, String encodedPassword) {
return false;
}
});
}
}
@@ -41,7 +41,7 @@ import static org.mockito.Mockito.verifyNoMoreInteractions;
* @since 5.0
*/
@ExtendWith(MockitoExtension.class)
public class DelegatingPasswordEncoderTests {
public class DelegatingPasswordEncoderTests extends AbstractPasswordEncoderValidationTests {
@Mock
private PasswordEncoder bcrypt;
@@ -64,8 +64,6 @@ public class DelegatingPasswordEncoderTests {
private Map<String, PasswordEncoder> delegates;
private DelegatingPasswordEncoder passwordEncoder;
private DelegatingPasswordEncoder onlySuffixPasswordEncoder;
private static final String NO_PASSWORD_ENCODER_MAPPED = "There is no password encoder mapped for the id 'unmapped'. "
@@ -81,8 +79,7 @@ public class DelegatingPasswordEncoderTests {
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);
setEncoder(new DelegatingPasswordEncoder(this.bcryptId, this.delegates));
this.onlySuffixPasswordEncoder = new DelegatingPasswordEncoder(this.bcryptId, this.delegates, "", "$");
}
@@ -149,14 +146,14 @@ public class DelegatingPasswordEncoderTests {
@Test
public void setDefaultPasswordEncoderForMatchesWhenNullThenIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> this.passwordEncoder.setDefaultPasswordEncoderForMatches(null));
.isThrownBy(() -> getEncoder(DelegatingPasswordEncoder.class).setDefaultPasswordEncoderForMatches(null));
}
@Test
public void matchesWhenCustomDefaultPasswordEncoderForMatchesThenDelegates() {
String encodedPassword = "{unmapped}" + this.rawPassword;
this.passwordEncoder.setDefaultPasswordEncoderForMatches(this.invalidId);
assertThat(this.passwordEncoder.matches(this.rawPassword, encodedPassword)).isFalse();
getEncoder(DelegatingPasswordEncoder.class).setDefaultPasswordEncoderForMatches(this.invalidId);
assertThat(getEncoder().matches(this.rawPassword, encodedPassword)).isFalse();
verify(this.invalidId).matches(this.rawPassword, encodedPassword);
verifyNoMoreInteractions(this.bcrypt, this.noop);
}
@@ -164,7 +161,7 @@ public class DelegatingPasswordEncoderTests {
@Test
public void encodeWhenValidThenUsesIdForEncode() {
given(this.bcrypt.encode(this.rawPassword)).willReturn(this.encodedPassword);
assertThat(this.passwordEncoder.encode(this.rawPassword)).isEqualTo(this.bcryptEncodedPassword);
assertThat(getEncoder().encode(this.rawPassword)).isEqualTo(this.bcryptEncodedPassword);
}
@Test
@@ -176,7 +173,7 @@ public class DelegatingPasswordEncoderTests {
@Test
public void matchesWhenBCryptThenDelegatesToBCrypt() {
given(this.bcrypt.matches(this.rawPassword, this.encodedPassword)).willReturn(true);
assertThat(this.passwordEncoder.matches(this.rawPassword, this.bcryptEncodedPassword)).isTrue();
assertThat(getEncoder().matches(this.rawPassword, this.bcryptEncodedPassword)).isTrue();
verify(this.bcrypt).matches(this.rawPassword, this.encodedPassword);
verifyNoMoreInteractions(this.noop);
}
@@ -192,7 +189,7 @@ public class DelegatingPasswordEncoderTests {
@Test
public void matchesWhenNoopThenDelegatesToNoop() {
given(this.noop.matches(this.rawPassword, this.encodedPassword)).willReturn(true);
assertThat(this.passwordEncoder.matches(this.rawPassword, this.noopEncodedPassword)).isTrue();
assertThat(getEncoder().matches(this.rawPassword, this.noopEncodedPassword)).isTrue();
verify(this.noop).matches(this.rawPassword, this.encodedPassword);
verifyNoMoreInteractions(this.bcrypt);
}
@@ -200,7 +197,7 @@ public class DelegatingPasswordEncoderTests {
@Test
public void matchesWhenUnMappedThenIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> this.passwordEncoder.matches(this.rawPassword, "{unmapped}" + this.rawPassword))
.isThrownBy(() -> getEncoder().matches(this.rawPassword, "{unmapped}" + this.rawPassword))
.withMessage(NO_PASSWORD_ENCODER_MAPPED);
verifyNoMoreInteractions(this.bcrypt, this.noop);
}
@@ -208,7 +205,7 @@ public class DelegatingPasswordEncoderTests {
@Test
public void matchesWhenNoClosingPrefixStringThenIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> this.passwordEncoder.matches(this.rawPassword, "{bcrypt" + this.rawPassword))
.isThrownBy(() -> getEncoder().matches(this.rawPassword, "{bcrypt" + this.rawPassword))
.withMessage(MALFORMED_PASSWORD_ENCODER_PREFIX);
verifyNoMoreInteractions(this.bcrypt, this.noop);
}
@@ -216,7 +213,7 @@ public class DelegatingPasswordEncoderTests {
@Test
public void matchesWhenNoStartingPrefixStringThenFalse() {
assertThatIllegalArgumentException()
.isThrownBy(() -> this.passwordEncoder.matches(this.rawPassword, "bcrypt}" + this.rawPassword))
.isThrownBy(() -> getEncoder().matches(this.rawPassword, "bcrypt}" + this.rawPassword))
.withMessage(MALFORMED_PASSWORD_ENCODER_PREFIX);
verifyNoMoreInteractions(this.bcrypt, this.noop);
}
@@ -224,7 +221,7 @@ public class DelegatingPasswordEncoderTests {
@Test
public void matchesWhenNoIdStringThenFalse() {
assertThatIllegalArgumentException()
.isThrownBy(() -> this.passwordEncoder.matches(this.rawPassword, "{}" + this.rawPassword))
.isThrownBy(() -> getEncoder().matches(this.rawPassword, "{}" + this.rawPassword))
.withMessage(MALFORMED_PASSWORD_ENCODER_PREFIX);
verifyNoMoreInteractions(this.bcrypt, this.noop);
}
@@ -232,7 +229,7 @@ public class DelegatingPasswordEncoderTests {
@Test
public void matchesWhenPrefixInMiddleThenFalse() {
assertThatIllegalArgumentException()
.isThrownBy(() -> this.passwordEncoder.matches(this.rawPassword, "invalid" + this.bcryptEncodedPassword))
.isThrownBy(() -> getEncoder().matches(this.rawPassword, "invalid" + this.bcryptEncodedPassword))
.isInstanceOf(IllegalArgumentException.class)
.withMessage(MALFORMED_PASSWORD_ENCODER_PREFIX);
verifyNoMoreInteractions(this.bcrypt, this.noop);
@@ -251,56 +248,51 @@ public class DelegatingPasswordEncoderTests {
@Test
public void matchesWhenNullIdThenDelegatesToInvalidId() {
this.delegates.put(null, this.invalidId);
this.passwordEncoder = new DelegatingPasswordEncoder(this.bcryptId, this.delegates);
setEncoder(new DelegatingPasswordEncoder(this.bcryptId, this.delegates));
given(this.invalidId.matches(this.rawPassword, this.encodedPassword)).willReturn(true);
assertThat(this.passwordEncoder.matches(this.rawPassword, this.encodedPassword)).isTrue();
assertThat(getEncoder().matches(this.rawPassword, this.encodedPassword)).isTrue();
verify(this.invalidId).matches(this.rawPassword, this.encodedPassword);
verifyNoMoreInteractions(this.bcrypt, this.noop);
}
@Test
public void matchesWhenRawPasswordNotNullAndEncodedPasswordNullThenThrowsIllegalArgumentException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.passwordEncoder.matches(this.rawPassword, null));
}
@Test
public void upgradeEncodingWhenEncodedPasswordNullThenTrue() {
assertThat(this.passwordEncoder.upgradeEncoding(null)).isTrue();
public void upgradeEncodingWhenEncodedPasswordNullThenFalse() {
assertThat(getEncoder().upgradeEncoding(null)).isFalse();
}
@Test
public void upgradeEncodingWhenNullIdThenTrue() {
assertThat(this.passwordEncoder.upgradeEncoding(this.encodedPassword)).isTrue();
assertThat(getEncoder().upgradeEncoding(this.encodedPassword)).isTrue();
}
@Test
public void upgradeEncodingWhenIdInvalidFormatThenTrue() {
assertThat(this.passwordEncoder.upgradeEncoding("{bcrypt" + this.encodedPassword)).isTrue();
assertThat(getEncoder().upgradeEncoding("{bcrypt" + this.encodedPassword)).isTrue();
}
@Test
public void upgradeEncodingWhenSameIdAndEncoderFalseThenEncoderDecidesFalse() {
assertThat(this.passwordEncoder.upgradeEncoding(this.bcryptEncodedPassword)).isFalse();
assertThat(getEncoder().upgradeEncoding(this.bcryptEncodedPassword)).isFalse();
verify(this.bcrypt).upgradeEncoding(this.encodedPassword);
}
@Test
public void upgradeEncodingWhenSameIdAndEncoderTrueThenEncoderDecidesTrue() {
given(this.bcrypt.upgradeEncoding(any())).willReturn(true);
assertThat(this.passwordEncoder.upgradeEncoding(this.bcryptEncodedPassword)).isTrue();
assertThat(getEncoder().upgradeEncoding(this.bcryptEncodedPassword)).isTrue();
verify(this.bcrypt).upgradeEncoding(this.encodedPassword);
}
@Test
public void upgradeEncodingWhenDifferentIdThenTrue() {
assertThat(this.passwordEncoder.upgradeEncoding(this.noopEncodedPassword)).isTrue();
assertThat(getEncoder().upgradeEncoding(this.noopEncodedPassword)).isTrue();
verifyNoMoreInteractions(this.bcrypt);
}
@Test
void matchesShouldThrowIllegalArgumentExceptionWhenNoPasswordEncoderIsMappedForTheId() {
assertThatIllegalArgumentException()
.isThrownBy(() -> this.passwordEncoder.matches("rawPassword", "prefixEncodedPassword"))
.isThrownBy(() -> getEncoder().matches("rawPassword", "prefixEncodedPassword"))
.isInstanceOf(IllegalArgumentException.class)
.withMessage(NO_PASSWORD_ENCODER_PREFIX);
verifyNoMoreInteractions(this.bcrypt, this.noop);
@@ -16,6 +16,7 @@
package org.springframework.security.crypto.password;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.security.crypto.keygen.KeyGenerators;
@@ -29,19 +30,22 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
* @author Luke Taylor
*/
@SuppressWarnings("deprecation")
public class LdapShaPasswordEncoderTests {
public class LdapShaPasswordEncoderTests extends AbstractPasswordEncoderValidationTests {
LdapShaPasswordEncoder sha = new LdapShaPasswordEncoder();
@BeforeEach
void setup() {
setEncoder(new LdapShaPasswordEncoder());
}
@Test
public void invalidPasswordFails() {
assertThat(this.sha.matches("wrongpassword", "{SHA}ddSFGmjXYPbZC+NXR2kCzBRjqiE=")).isFalse();
assertThat(getEncoder().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();
assertThat(getEncoder().matches("wrongpassword", "{SSHA}25ro4PKC8jhQZ26jVsozhX/xaP0suHgX")).isFalse();
assertThat(getEncoder().matches("wrongpassword", "{SSHA}PQy2j+6n5ytA+YlAKkM8Fh4p6u2JxfVd")).isFalse();
}
/**
@@ -49,12 +53,13 @@ public class LdapShaPasswordEncoderTests {
*/
@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();
LdapShaPasswordEncoder ldap = getEncoder();
ldap.setForceLowerCasePrefix(false);
assertThat(getEncoder().matches("boabspasswurd", "{SHA}ddSFGmjXYPbZC+NXR2kCzBRjqiE=")).isTrue();
assertThat(getEncoder().matches("boabspasswurd", "{sha}ddSFGmjXYPbZC+NXR2kCzBRjqiE=")).isTrue();
ldap.setForceLowerCasePrefix(true);
assertThat(getEncoder().matches("boabspasswurd", "{SHA}ddSFGmjXYPbZC+NXR2kCzBRjqiE=")).isTrue();
assertThat(getEncoder().matches("boabspasswurd", "{sha}ddSFGmjXYPbZC+NXR2kCzBRjqiE=")).isTrue();
}
/**
@@ -62,47 +67,49 @@ public class LdapShaPasswordEncoderTests {
*/
@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();
LdapShaPasswordEncoder ldap = getEncoder();
ldap.setForceLowerCasePrefix(false);
assertThat(getEncoder().matches("boabspasswurd", "{SSHA}25ro4PKC8jhQZ26jVsozhX/xaP0suHgX")).isTrue();
assertThat(getEncoder().matches("boabspasswurd", "{ssha}PQy2j+6n5ytA+YlAKkM8Fh4p6u2JxfVd")).isTrue();
ldap.setForceLowerCasePrefix(true);
assertThat(getEncoder().matches("boabspasswurd", "{SSHA}25ro4PKC8jhQZ26jVsozhX/xaP0suHgX")).isTrue();
assertThat(getEncoder().matches("boabspasswurd", "{ssha}PQy2j+6n5ytA+YlAKkM8Fh4p6u2JxfVd")).isTrue();
}
@Test
// SEC-1031
public void fullLengthOfHashIsUsedInComparison() {
assertThat(this.sha.matches("boabspasswurd", "{SSHA}25ro4PKC8jhQZ26jVsozhX/xaP0suHgX")).isTrue();
assertThat(getEncoder().matches("boabspasswurd", "{SSHA}25ro4PKC8jhQZ26jVsozhX/xaP0suHgX")).isTrue();
// Change the first hash character from '2' to '3'
assertThat(this.sha.matches("boabspasswurd", "{SSHA}35ro4PKC8jhQZ26jVsozhX/xaP0suHgX")).isFalse();
assertThat(getEncoder().matches("boabspasswurd", "{SSHA}35ro4PKC8jhQZ26jVsozhX/xaP0suHgX")).isFalse();
// Change the last hash character from 'X' to 'Y'
assertThat(this.sha.matches("boabspasswurd", "{SSHA}25ro4PKC8jhQZ26jVsozhX/xaP0suHgY")).isFalse();
assertThat(getEncoder().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}"));
LdapShaPasswordEncoder ldap = getEncoder();
ldap.setForceLowerCasePrefix(false);
assertThat(ldap.encode("somepassword").startsWith("{SSHA}"));
ldap.setForceLowerCasePrefix(true);
assertThat(ldap.encode("somepassword").startsWith("{ssha}"));
setEncoder(new LdapShaPasswordEncoder(KeyGenerators.shared(0)));
ldap.setForceLowerCasePrefix(false);
assertThat(getEncoder().encode("somepassword").startsWith("{SHA}"));
ldap.setForceLowerCasePrefix(true);
assertThat(getEncoder().encode("somepassword").startsWith("{SSHA}"));
}
@Test
public void invalidPrefixIsRejected() {
assertThatIllegalArgumentException().isThrownBy(() -> this.sha.matches("somepassword", "{MD9}xxxxxxxxxx"));
assertThatIllegalArgumentException().isThrownBy(() -> getEncoder().matches("somepassword", "{MD9}xxxxxxxxxx"));
}
@Test
public void malformedPrefixIsRejected() {
// No right brace
assertThatIllegalArgumentException()
.isThrownBy(() -> this.sha.matches("somepassword", "{SSHA25ro4PKC8jhQZ26jVsozhX/xaP0suHgX"));
.isThrownBy(() -> getEncoder().matches("somepassword", "{SSHA25ro4PKC8jhQZ26jVsozhX/xaP0suHgX"));
}
}
@@ -16,59 +16,58 @@
package org.springframework.security.crypto.password;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
@SuppressWarnings("deprecation")
public class Md4PasswordEncoderTests {
public class Md4PasswordEncoderTests extends AbstractPasswordEncoderValidationTests {
@BeforeEach
void setup() {
setEncoder(new Md4PasswordEncoder());
}
@Test
public void matchesWhenEncodedPasswordNullThenFalse() {
assertThat(getEncoder().matches("raw", null)).isFalse();
}
@Test
public void matchesWhenEncodedPasswordEmptyThenFalse() {
assertThat(getEncoder().matches("raw", "")).isFalse();
}
@Test
public void testEncodeUnsaltedPassword() {
Md4PasswordEncoder md4 = new Md4PasswordEncoder();
Md4PasswordEncoder md4 = getEncoder();
md4.setEncodeHashAsBase64(true);
assertThat(md4.matches("ww_uni123", "8zobtq72iAt0W6KNqavGwg==")).isTrue();
}
@Test
public void testEncodeSaltedPassword() {
Md4PasswordEncoder md4 = new Md4PasswordEncoder();
Md4PasswordEncoder md4 = getEncoder();
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();
assertThat(getEncoder().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();
String encodedPassword = getEncoder().encode(rawPassword);
assertThat(getEncoder().matches(rawPassword, encodedPassword)).isTrue();
}
@Test
public void javadocWhenHasSaltThenMatches() {
Md4PasswordEncoder encoder = new Md4PasswordEncoder();
assertThat(encoder.matches("password", "{thisissalt}6cc7924dad12ade79dfb99e424f25260"));
assertThat(getEncoder().matches("password", "{thisissalt}6cc7924dad12ade79dfb99e424f25260"));
}
}
@@ -16,6 +16,7 @@
package org.springframework.security.crypto.password;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
@@ -32,70 +33,69 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
* @author Luke Taylor
*/
@SuppressWarnings("deprecation")
public class MessageDigestPasswordEncoderTests {
public class MessageDigestPasswordEncoderTests extends AbstractPasswordEncoderValidationTests {
@BeforeEach
void setup() {
setEncoder(new MessageDigestPasswordEncoder("MD5"));
}
@Test
public void md5BasicFunctionality() {
MessageDigestPasswordEncoder pe = new MessageDigestPasswordEncoder("MD5");
String raw = "abc123";
assertThat(pe.matches(raw, "{THIS_IS_A_SALT}a68aafd90299d0b137de28fb4bb68573")).isTrue();
assertThat(getEncoder().matches(raw, "{THIS_IS_A_SALT}a68aafd90299d0b137de28fb4bb68573")).isTrue();
}
@Test
public void md5NonAsciiPasswordHasCorrectHash() {
MessageDigestPasswordEncoder pe = new MessageDigestPasswordEncoder("MD5");
// $ echo -n "??" | md5
// 7eca689f0d3389d9dea66ae112e5cfd7
assertThat(pe.matches("\u4F60\u597d", "7eca689f0d3389d9dea66ae112e5cfd7")).isTrue();
assertThat(getEncoder().matches("\u4F60\u597d", "7eca689f0d3389d9dea66ae112e5cfd7")).isTrue();
}
@Test
public void md5Base64() {
MessageDigestPasswordEncoder pe = new MessageDigestPasswordEncoder("MD5");
MessageDigestPasswordEncoder pe = getEncoder();
pe.setEncodeHashAsBase64(true);
assertThat(pe.matches("abc123", "{THIS_IS_A_SALT}poqv2QKZ0LE33ij7S7aFcw==")).isTrue();
assertThat(getEncoder().matches("abc123", "{THIS_IS_A_SALT}poqv2QKZ0LE33ij7S7aFcw==")).isTrue();
}
@Test
public void md5StretchFactorIsProcessedCorrectly() {
MessageDigestPasswordEncoder pe = new MessageDigestPasswordEncoder("MD5");
MessageDigestPasswordEncoder pe = getEncoder();
pe.setIterations(2);
// Calculate value using:
// echo -n password{salt} | openssl md5 -binary | openssl md5
assertThat(pe.matches("password", "{salt}eb753fb0c370582b4ee01b30f304b9fc")).isTrue();
assertThat(getEncoder().matches("password", "{salt}eb753fb0c370582b4ee01b30f304b9fc")).isTrue();
}
@Test
public void md5MatchesWhenNullSalt() {
MessageDigestPasswordEncoder pe = new MessageDigestPasswordEncoder("MD5");
assertThat(pe.matches("password", "5f4dcc3b5aa765d61d8327deb882cf99")).isTrue();
assertThat(getEncoder().matches("password", "5f4dcc3b5aa765d61d8327deb882cf99")).isTrue();
}
@Test
public void md5MatchesWhenEmptySalt() {
MessageDigestPasswordEncoder pe = new MessageDigestPasswordEncoder("MD5");
assertThat(pe.matches("password", "{}f1026a66095fc2058c1f8771ed05d6da")).isTrue();
assertThat(getEncoder().matches("password", "{}f1026a66095fc2058c1f8771ed05d6da")).isTrue();
}
@Test
public void md5MatchesWhenHasSalt() {
MessageDigestPasswordEncoder pe = new MessageDigestPasswordEncoder("MD5");
assertThat(pe.matches("password", "{salt}ce421738b1c5540836bdc8ff707f1572")).isTrue();
assertThat(getEncoder().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();
String encode = getEncoder().encode(rawPassword);
assertThat(getEncoder().matches(rawPassword, encode)).isTrue();
}
@Test
public void testBasicFunctionality() {
MessageDigestPasswordEncoder pe = new MessageDigestPasswordEncoder("SHA-1");
setEncoder(new MessageDigestPasswordEncoder("SHA-1"));
String raw = "abc123";
assertThat(pe.matches(raw, "{THIS_IS_A_SALT}b2f50ffcbd3407fe9415c062d55f54731f340d32"));
assertThat(getEncoder().matches(raw, "{THIS_IS_A_SALT}b2f50ffcbd3407fe9415c062d55f54731f340d32"));
}
@Test
@@ -103,14 +103,15 @@ public class MessageDigestPasswordEncoderTests {
MessageDigestPasswordEncoder pe = new MessageDigestPasswordEncoder("SHA-1");
pe.setEncodeHashAsBase64(true);
String raw = "abc123";
assertThat(pe.matches(raw, "{THIS_IS_A_SALT}b2f50ffcbd3407fe9415c062d55f54731f340d32"));
assertThat(getEncoder().matches(raw, "{THIS_IS_A_SALT}b2f50ffcbd3407fe9415c062d55f54731f340d32"));
}
@Test
public void test256() {
MessageDigestPasswordEncoder pe = new MessageDigestPasswordEncoder("SHA-1");
String raw = "abc123";
assertThat(pe.matches(raw, "{THIS_IS_A_SALT}4b79b7de23eb23b78cc5ede227d532b8a51f89b2ec166f808af76b0dbedc47d7"));
assertThat(getEncoder().matches(raw,
"{THIS_IS_A_SALT}4b79b7de23eb23b78cc5ede227d532b8a51f89b2ec166f808af76b0dbedc47d7"));
}
@Test
@@ -0,0 +1,33 @@
/*
* Copyright 2002-2025 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
*
* https://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.jupiter.api.BeforeEach;
/**
* Test {@link NoOpPasswordEncoder}.
*
* @author Rob Winch
*/
class NoOpPasswordEncoderTests extends AbstractPasswordEncoderValidationTests {
@BeforeEach
void setup() {
setEncoder(NoOpPasswordEncoder.getInstance());
}
}