diff --git a/crypto/src/main/java/org/springframework/security/crypto/factory/PasswordEncoderFactories.java b/crypto/src/main/java/org/springframework/security/crypto/factory/PasswordEncoderFactories.java
index 7704f6f210..b6c6f2f3e5 100644
--- a/crypto/src/main/java/org/springframework/security/crypto/factory/PasswordEncoderFactories.java
+++ b/crypto/src/main/java/org/springframework/security/crypto/factory/PasswordEncoderFactories.java
@@ -24,7 +24,6 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder;
-import org.springframework.security.crypto.password4j.Password4jPasswordEncoder;
import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder;
/**
@@ -66,10 +65,6 @@ public final class PasswordEncoderFactories {
*
argon2 - {@link Argon2PasswordEncoder#defaultsForSpringSecurity_v5_2()}
* argon2@SpringSecurity_v5_8 -
* {@link Argon2PasswordEncoder#defaultsForSpringSecurity_v5_8()}
- * password4j-bcrypt - {@link Password4jPasswordEncoder} with BCrypt
- * password4j-scrypt - {@link Password4jPasswordEncoder} with SCrypt
- * password4j-argon2 - {@link Password4jPasswordEncoder} with Argon2
- * password4j-pbkdf2 - {@link Password4jPasswordEncoder} with PBKDF2
*
* @return the {@link PasswordEncoder} to use
*/
@@ -92,14 +87,6 @@ public final class PasswordEncoderFactories {
encoders.put("sha256", new org.springframework.security.crypto.password.StandardPasswordEncoder());
encoders.put("argon2", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_2());
encoders.put("argon2@SpringSecurity_v5_8", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8());
-
- // Password4j implementations
- encoders.put("password4j-bcrypt", Password4jPasswordEncoder.bcrypt(10));
- encoders.put("password4j-scrypt", Password4jPasswordEncoder.scrypt(16384, 8, 1, 32));
- encoders.put("password4j-argon2", Password4jPasswordEncoder.argon2(65536, 3, 4, 32,
- com.password4j.types.Argon2.ID));
- encoders.put("password4j-pbkdf2", Password4jPasswordEncoder.pbkdf2(310000, 32));
-
return new DelegatingPasswordEncoder(encodingId, encoders);
}
diff --git a/crypto/src/main/java/org/springframework/security/crypto/password4j/Password4jPasswordEncoder.java b/crypto/src/main/java/org/springframework/security/crypto/password4j/Password4jPasswordEncoder.java
index 402fb4d42b..36d286584f 100644
--- a/crypto/src/main/java/org/springframework/security/crypto/password4j/Password4jPasswordEncoder.java
+++ b/crypto/src/main/java/org/springframework/security/crypto/password4j/Password4jPasswordEncoder.java
@@ -16,24 +16,54 @@
package org.springframework.security.crypto.password4j;
-import com.password4j.*;
-import com.password4j.types.Argon2;
+import com.password4j.AlgorithmFinder;
+import com.password4j.Hash;
+import com.password4j.HashingFunction;
+import com.password4j.Password;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
+
import org.springframework.security.crypto.password.AbstractValidatingPasswordEncoder;
import org.springframework.util.Assert;
/**
- * Implementation of {@link org.springframework.security.crypto.password.PasswordEncoder} that uses the Password4j library.
- * This encoder supports multiple password hashing algorithms including BCrypt, SCrypt, Argon2, and PBKDF2.
+ * Implementation of {@link org.springframework.security.crypto.password.PasswordEncoder}
+ * that uses the Password4j library. This encoder supports multiple password hashing
+ * algorithms including BCrypt, SCrypt, Argon2, and PBKDF2.
*
- * The encoder determines the algorithm used based on the algorithm type specified during construction.
- * For verification, it can automatically detect the algorithm used in existing hashes.
+ *
+ * The encoder uses the provided {@link HashingFunction} for both encoding and
+ * verification. Password4j can automatically detect the algorithm used in existing hashes
+ * during verification.
+ *
*
- * This implementation is thread-safe and can be shared across multiple threads.
+ *
+ * This implementation is thread-safe and can be shared across multiple threads.
+ *
+ *
+ *
+ * Usage Examples:
+ *
+ * {@code
+ * // Using default algorithms from AlgorithmFinder (recommended approach)
+ * PasswordEncoder bcryptEncoder = new Password4jPasswordEncoder(AlgorithmFinder.getBcryptInstance());
+ * PasswordEncoder argon2Encoder = new Password4jPasswordEncoder(AlgorithmFinder.getArgon2Instance());
+ * PasswordEncoder scryptEncoder = new Password4jPasswordEncoder(AlgorithmFinder.getScryptInstance());
+ * PasswordEncoder pbkdf2Encoder = new Password4jPasswordEncoder(AlgorithmFinder.getPBKDF2Instance());
+ *
+ * // Using customized algorithm parameters
+ * PasswordEncoder customBcrypt = new Password4jPasswordEncoder(BcryptFunction.getInstance(12));
+ * PasswordEncoder customArgon2 = new Password4jPasswordEncoder(
+ * Argon2Function.getInstance(65536, 3, 4, 32, Argon2.ID));
+ * PasswordEncoder customScrypt = new Password4jPasswordEncoder(
+ * ScryptFunction.getInstance(32768, 8, 1, 32));
+ * PasswordEncoder customPbkdf2 = new Password4jPasswordEncoder(
+ * CompressedPBKDF2Function.getInstance("SHA256", 310000, 32));
+ * }
*
* @author Mehrdad Bozorgmehr
- * @since 6.5
+ * @since 7.0
+ * @see AlgorithmFinder
*/
public class Password4jPasswordEncoder extends AbstractValidatingPasswordEncoder {
@@ -41,146 +71,38 @@ public class Password4jPasswordEncoder extends AbstractValidatingPasswordEncoder
private final HashingFunction hashingFunction;
- private final Password4jAlgorithm algorithm;
-
-
/**
- * Enumeration of supported Password4j algorithms.
- */
- public enum Password4jAlgorithm {
- /**
- * BCrypt algorithm.
- */
- BCRYPT,
- /**
- * SCrypt algorithm.
- */
- SCRYPT,
- /**
- * Argon2 algorithm.
- */
- ARGON2,
- /**
- * PBKDF2 algorithm.
- */
- PBKDF2,
- /**
- * Compressed PBKDF2 algorithm.
- */
- COMPRESSED_PBKDF2
- }
-
- /**
- * Constructs a Password4j password encoder with the default BCrypt algorithm.
- */
- public Password4jPasswordEncoder() {
- this(Password4jAlgorithm.BCRYPT);
- }
-
- /**
- * Constructs a Password4j password encoder with the specified algorithm using default parameters.
+ * Constructs a Password4j password encoder with the specified hashing function.
*
- * @param algorithm the password hashing algorithm to use
- */
- public Password4jPasswordEncoder(Password4jAlgorithm algorithm) {
- Assert.notNull(algorithm, "algorithm cannot be null");
- this.algorithm = algorithm;
- this.hashingFunction = createDefaultHashingFunction(algorithm);
- }
-
- /**
- * Constructs a Password4j password encoder with a custom hashing function.
+ *
+ * It is recommended to use password4j's {@link AlgorithmFinder} to obtain default
+ * instances with secure configurations:
+ *
+ *
+ * - {@code AlgorithmFinder.getBcryptInstance()} - BCrypt with default settings
+ * - {@code AlgorithmFinder.getArgon2Instance()} - Argon2 with default settings
+ * - {@code AlgorithmFinder.getScryptInstance()} - SCrypt with default settings
+ * - {@code AlgorithmFinder.getPBKDF2Instance()} - PBKDF2 with default settings
+ *
*
- * @param hashingFunction the custom hashing function to use
- * @param algorithm the password hashing algorithm type
+ *
+ * For custom configurations, you can create specific function instances:
+ *
+ *
+ * - {@code BcryptFunction.getInstance(12)} - BCrypt with 12 rounds
+ * - {@code Argon2Function.getInstance(65536, 3, 4, 32, Argon2.ID)} - Custom
+ * Argon2
+ * - {@code ScryptFunction.getInstance(16384, 8, 1, 32)} - Custom SCrypt
+ * - {@code CompressedPBKDF2Function.getInstance("SHA256", 310000, 32)} - Custom
+ * PBKDF2
+ *
+ * @param hashingFunction the hashing function to use for encoding passwords, must not
+ * be null
+ * @throws IllegalArgumentException if hashingFunction is null
*/
- public Password4jPasswordEncoder(HashingFunction hashingFunction, Password4jAlgorithm algorithm) {
+ public Password4jPasswordEncoder(HashingFunction hashingFunction) {
Assert.notNull(hashingFunction, "hashingFunction cannot be null");
- Assert.notNull(algorithm, "algorithm cannot be null");
this.hashingFunction = hashingFunction;
- this.algorithm = algorithm;
- }
-
- /**
- * Creates a Password4j password encoder with BCrypt algorithm and specified rounds.
- *
- * @param rounds the number of rounds (cost factor) for BCrypt
- * @return a new Password4j password encoder
- */
- public static Password4jPasswordEncoder bcrypt(int rounds) {
- return new Password4jPasswordEncoder(BcryptFunction.getInstance(rounds), Password4jAlgorithm.BCRYPT);
- }
-
- /**
- * Creates a Password4j password encoder with SCrypt algorithm and specified parameters.
- *
- * @param workFactor the work factor (N parameter)
- * @param resources the resources (r parameter)
- * @param parallelization the parallelization (p parameter)
- * @param derivedKeyLength the derived key length
- * @return a new Password4j password encoder
- */
- public static Password4jPasswordEncoder scrypt(int workFactor, int resources, int parallelization, int derivedKeyLength) {
- return new Password4jPasswordEncoder(
- ScryptFunction.getInstance(workFactor, resources, parallelization, derivedKeyLength),
- Password4jAlgorithm.SCRYPT
- );
- }
-
- /**
- * Creates a Password4j password encoder with Argon2 algorithm and specified parameters.
- *
- * @param memory the memory cost
- * @param iterations the number of iterations
- * @param parallelism the parallelism
- * @param outputLength the output length
- * @param type the Argon2 type
- * @return a new Password4j password encoder
- */
- public static Password4jPasswordEncoder argon2(int memory, int iterations, int parallelism, int outputLength, Argon2 type) {
- return new Password4jPasswordEncoder(
- Argon2Function.getInstance(memory, iterations, parallelism, outputLength, type),
- Password4jAlgorithm.ARGON2
- );
- }
-
- /**
- * Creates a Password4j password encoder with PBKDF2 algorithm and specified parameters.
- *
- * @param iterations the number of iterations
- * @param derivedKeyLength the derived key length
- * @return a new Password4j password encoder
- */
- public static Password4jPasswordEncoder pbkdf2(int iterations, int derivedKeyLength) {
- return new Password4jPasswordEncoder(
- CompressedPBKDF2Function.getInstance("SHA256", iterations, derivedKeyLength),
- Password4jAlgorithm.PBKDF2
- );
- }
-
- /**
- * Creates a Password4j password encoder with compressed PBKDF2 algorithm.
- *
- * @param iterations the number of iterations
- * @param derivedKeyLength the derived key length
- * @return a new Password4j password encoder
- */
- public static Password4jPasswordEncoder compressedPbkdf2(int iterations, int derivedKeyLength) {
- return new Password4jPasswordEncoder(
- CompressedPBKDF2Function.getInstance("SHA256", iterations, derivedKeyLength),
- Password4jAlgorithm.COMPRESSED_PBKDF2
- );
- }
-
- /**
- * Creates a Password4j password encoder with default settings for Spring Security v5.8+.
- * This uses BCrypt with 10 rounds.
- *
- * @return a new Password4j password encoder with recommended defaults
- * @since 6.5
- */
- public static Password4jPasswordEncoder defaultsForSpringSecurity() {
- return bcrypt(10);
}
@Override
@@ -188,7 +110,8 @@ public class Password4jPasswordEncoder extends AbstractValidatingPasswordEncoder
try {
Hash hash = Password.hash(rawPassword).with(this.hashingFunction);
return hash.getResult();
- } catch (Exception ex) {
+ }
+ catch (Exception ex) {
throw new IllegalStateException("Failed to encode password using Password4j", ex);
}
}
@@ -198,7 +121,8 @@ public class Password4jPasswordEncoder extends AbstractValidatingPasswordEncoder
try {
// Use the specific hashing function for verification
return Password.check(rawPassword, encodedPassword).with(this.hashingFunction);
- } catch (Exception ex) {
+ }
+ catch (Exception ex) {
this.logger.warn("Password verification failed for encoded password: " + encodedPassword, ex);
return false;
}
@@ -211,39 +135,4 @@ public class Password4jPasswordEncoder extends AbstractValidatingPasswordEncoder
return false;
}
- /**
- * Creates a default hashing function for the specified algorithm.
- *
- * @param algorithm the password hashing algorithm
- * @return the default hashing function
- */
- private static HashingFunction createDefaultHashingFunction(Password4jAlgorithm algorithm) {
- return switch (algorithm) {
- case BCRYPT -> BcryptFunction.getInstance(10); // Default 10 rounds
- case SCRYPT -> ScryptFunction.getInstance(16384, 8, 1, 32); // Default parameters
- case ARGON2 -> Argon2Function.getInstance(65536, 3, 4, 32, Argon2.ID); // Default parameters
- case PBKDF2 ->
- CompressedPBKDF2Function.getInstance("SHA256", 310000, 32); // Use compressed format for self-contained encoding
- case COMPRESSED_PBKDF2 -> CompressedPBKDF2Function.getInstance("SHA256", 310000, 32);
- };
- }
-
- /**
- * Gets the algorithm used by this encoder.
- *
- * @return the password hashing algorithm
- */
- public Password4jAlgorithm getAlgorithm() {
- return this.algorithm;
- }
-
- /**
- * Gets the hashing function used by this encoder.
- *
- * @return the hashing function
- */
- public HashingFunction getHashingFunction() {
- return this.hashingFunction;
- }
-
}
diff --git a/crypto/src/main/java/org/springframework/security/crypto/password4j/package-info.java b/crypto/src/main/java/org/springframework/security/crypto/password4j/package-info.java
index f15bf9e10b..7310e80b8f 100644
--- a/crypto/src/main/java/org/springframework/security/crypto/password4j/package-info.java
+++ b/crypto/src/main/java/org/springframework/security/crypto/password4j/package-info.java
@@ -14,7 +14,6 @@
* limitations under the License.
*/
-
@NullMarked
package org.springframework.security.crypto.password4j;
diff --git a/crypto/src/test/java/org/springframework/security/crypto/password4j/Password4jPasswordEncoderTests.java b/crypto/src/test/java/org/springframework/security/crypto/password4j/Password4jPasswordEncoderTests.java
index 93c6e90f45..f4bec5dd4a 100644
--- a/crypto/src/test/java/org/springframework/security/crypto/password4j/Password4jPasswordEncoderTests.java
+++ b/crypto/src/test/java/org/springframework/security/crypto/password4j/Password4jPasswordEncoderTests.java
@@ -16,17 +16,10 @@
package org.springframework.security.crypto.password4j;
-import com.password4j.Argon2Function;
+import com.password4j.AlgorithmFinder;
import com.password4j.BcryptFunction;
-import com.password4j.CompressedPBKDF2Function;
-import com.password4j.ScryptFunction;
-import com.password4j.types.Argon2;
-import org.junit.jupiter.api.RepeatedTest;
+import com.password4j.HashingFunction;
import org.junit.jupiter.api.Test;
-import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.CsvSource;
-import org.junit.jupiter.params.provider.EnumSource;
-import org.junit.jupiter.params.provider.ValueSource;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
@@ -38,559 +31,111 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
*/
class Password4jPasswordEncoderTests {
- private static final String PASSWORD = "password";
- private static final String WRONG_PASSWORD = "wrongpassword";
- private static final String UNICODE_PASSWORD = "пароль123🔐";
- private static final String LONG_PASSWORD = "a".repeat(1000);
+ private static final String PASSWORD = "password";
+
+ private static final String WRONG_PASSWORD = "wrongpassword";
+
+ // Constructor Tests
+ @Test
+ void constructorWithNullHashingFunctionShouldThrowException() {
+ assertThatIllegalArgumentException().isThrownBy(() -> new Password4jPasswordEncoder(null))
+ .withMessage("hashingFunction cannot be null");
+ }
+
+ @Test
+ void constructorWithValidHashingFunctionShouldWork() {
+ HashingFunction hashingFunction = BcryptFunction.getInstance(10);
+ Password4jPasswordEncoder encoder = new Password4jPasswordEncoder(hashingFunction);
+ assertThat(encoder).isNotNull();
+ }
+
+ // Basic functionality tests with real HashingFunction instances
+ @Test
+ void encodeShouldReturnNonNullHashedPassword() {
+ HashingFunction hashingFunction = BcryptFunction.getInstance(4); // Use low cost
+ // for faster
+ // tests
+ Password4jPasswordEncoder encoder = new Password4jPasswordEncoder(hashingFunction);
+
+ String result = encoder.encode(PASSWORD);
+
+ assertThat(result).isNotNull().isNotEqualTo(PASSWORD);
+ }
+
+ @Test
+ void matchesShouldReturnTrueForValidPassword() {
+ HashingFunction hashingFunction = BcryptFunction.getInstance(4); // Use low cost
+ // for faster
+ // tests
+ Password4jPasswordEncoder encoder = new Password4jPasswordEncoder(hashingFunction);
+
+ String encoded = encoder.encode(PASSWORD);
+ boolean result = encoder.matches(PASSWORD, encoded);
+
+ assertThat(result).isTrue();
+ }
+
+ @Test
+ void matchesShouldReturnFalseForInvalidPassword() {
+ HashingFunction hashingFunction = BcryptFunction.getInstance(4); // Use low cost
+ // for faster
+ // tests
+ Password4jPasswordEncoder encoder = new Password4jPasswordEncoder(hashingFunction);
+
+ String encoded = encoder.encode(PASSWORD);
+ boolean result = encoder.matches(WRONG_PASSWORD, encoded);
+
+ assertThat(result).isFalse();
+ }
+
+ @Test
+ void matchesShouldReturnFalseForMalformedHash() {
+ HashingFunction hashingFunction = BcryptFunction.getInstance(4);
+ Password4jPasswordEncoder encoder = new Password4jPasswordEncoder(hashingFunction);
+
+ // Test with malformed hash that should cause Password4j to throw an exception
+ boolean result = encoder.matches(PASSWORD, "invalid-hash-format");
+
+ assertThat(result).isFalse();
+ }
+
+ @Test
+ void upgradeEncodingShouldReturnFalse() {
+ HashingFunction hashingFunction = BcryptFunction.getInstance(4);
+ Password4jPasswordEncoder encoder = new Password4jPasswordEncoder(hashingFunction);
+
+ String encoded = encoder.encode(PASSWORD);
+ boolean result = encoder.upgradeEncoding(encoded);
+
+ assertThat(result).isFalse();
+ }
+
+ // AlgorithmFinder Sanity Check Tests
+ @Test
+ void algorithmFinderBcryptSanityCheck() {
+ Password4jPasswordEncoder encoder = new Password4jPasswordEncoder(AlgorithmFinder.getBcryptInstance());
+
+ String encoded = encoder.encode(PASSWORD);
+ assertThat(encoder.matches(PASSWORD, encoded)).isTrue();
+ assertThat(encoder.matches(WRONG_PASSWORD, encoded)).isFalse();
+ }
+
+ @Test
+ void algorithmFinderArgon2SanityCheck() {
+ Password4jPasswordEncoder encoder = new Password4jPasswordEncoder(AlgorithmFinder.getArgon2Instance());
+
+ String encoded = encoder.encode(PASSWORD);
+ assertThat(encoder.matches(PASSWORD, encoded)).isTrue();
+ assertThat(encoder.matches(WRONG_PASSWORD, encoded)).isFalse();
+ }
+
+ @Test
+ void algorithmFinderScryptSanityCheck() {
+ Password4jPasswordEncoder encoder = new Password4jPasswordEncoder(AlgorithmFinder.getScryptInstance());
+
+ String encoded = encoder.encode(PASSWORD);
+ assertThat(encoder.matches(PASSWORD, encoded)).isTrue();
+ assertThat(encoder.matches(WRONG_PASSWORD, encoded)).isFalse();
+ }
- // Constructor Tests
- @Test
- void constructorWithNullAlgorithmShouldThrowException() {
- assertThatIllegalArgumentException()
- .isThrownBy(() -> new Password4jPasswordEncoder(null))
- .withMessage("algorithm cannot be null");
- }
-
- @Test
- void constructorWithNullHashingFunctionShouldThrowException() {
- assertThatIllegalArgumentException()
- .isThrownBy(() -> new Password4jPasswordEncoder(null, Password4jPasswordEncoder.Password4jAlgorithm.BCRYPT))
- .withMessage("hashingFunction cannot be null");
- }
-
- @Test
- void constructorWithNullAlgorithmAndValidHashingFunctionShouldThrowException() {
- BcryptFunction function = BcryptFunction.getInstance(10);
- assertThatIllegalArgumentException()
- .isThrownBy(() -> new Password4jPasswordEncoder(function, null))
- .withMessage("algorithm cannot be null");
- }
-
- @Test
- void defaultConstructorShouldUseBCrypt() {
- Password4jPasswordEncoder encoder = new Password4jPasswordEncoder();
- assertThat(encoder.getAlgorithm()).isEqualTo(Password4jPasswordEncoder.Password4jAlgorithm.BCRYPT);
- assertThat(encoder.getHashingFunction()).isInstanceOf(BcryptFunction.class);
- }
-
- // BCrypt Tests
- @Test
- void bcryptEncoderShouldEncodeAndVerifyPasswords() {
- Password4jPasswordEncoder encoder = Password4jPasswordEncoder.bcrypt(10);
-
- String encoded = encoder.encode(PASSWORD);
- assertThat(encoded)
- .isNotNull()
- .isNotEqualTo(PASSWORD)
- .startsWith("$2b$10$");// Password4j uses $2b$ format
-
- assertThat(encoder.matches(PASSWORD, encoded)).isTrue();
- assertThat(encoder.matches(WRONG_PASSWORD, encoded)).isFalse();
- assertThat(encoder.matches(null, encoded)).isFalse();
- assertThat(encoder.matches(PASSWORD, null)).isFalse();
- }
-
- @ParameterizedTest
- @ValueSource(ints = {4, 6, 8, 10, 12, 14})
- void bcryptWithDifferentRoundsShouldWork(int rounds) {
- Password4jPasswordEncoder encoder = Password4jPasswordEncoder.bcrypt(rounds);
-
- String encoded = encoder.encode(PASSWORD);
- assertThat(encoded).startsWith("$2b$" + String.format("%02d", rounds) + "$");
- assertThat(encoder.matches(PASSWORD, encoded)).isTrue();
- }
-
- @Test
- void bcryptShouldProduceDifferentHashesForSamePassword() {
- Password4jPasswordEncoder encoder = Password4jPasswordEncoder.bcrypt(10);
-
- String hash1 = encoder.encode(PASSWORD);
- String hash2 = encoder.encode(PASSWORD);
-
- assertThat(hash1).isNotEqualTo(hash2);
- assertThat(encoder.matches(PASSWORD, hash1)).isTrue();
- assertThat(encoder.matches(PASSWORD, hash2)).isTrue();
- }
-
- // SCrypt Tests
- @Test
- void scryptEncoderShouldEncodeAndVerifyPasswords() {
- Password4jPasswordEncoder encoder = Password4jPasswordEncoder.scrypt(16384, 8, 1, 32);
-
- String encoded = encoder.encode(PASSWORD);
- assertThat(encoded).isNotNull().isNotEqualTo(PASSWORD);
-
- assertThat(encoder.matches(PASSWORD, encoded)).isTrue();
- assertThat(encoder.matches(WRONG_PASSWORD, encoded)).isFalse();
- }
-
- @Test
- void scryptWithDifferentParametersShouldWork() {
- Password4jPasswordEncoder encoder1 = Password4jPasswordEncoder.scrypt(8192, 8, 1, 32);
- Password4jPasswordEncoder encoder2 = Password4jPasswordEncoder.scrypt(16384, 16, 2, 64);
-
- String hash1 = encoder1.encode(PASSWORD);
- String hash2 = encoder2.encode(PASSWORD);
-
- assertThat(encoder1.matches(PASSWORD, hash1)).isTrue();
- assertThat(encoder2.matches(PASSWORD, hash2)).isTrue();
- assertThat(hash1).isNotEqualTo(hash2);
- }
-
- // Argon2 Tests
- @Test
- void argon2EncoderShouldEncodeAndVerifyPasswords() {
- Password4jPasswordEncoder encoder = Password4jPasswordEncoder.argon2(
- 65536, 3, 4, 32, Argon2.ID);
-
- String encoded = encoder.encode(PASSWORD);
- assertThat(encoded)
- .isNotNull()
- .isNotEqualTo(PASSWORD)
- .startsWith("$argon2id$");
-
- assertThat(encoder.matches(PASSWORD, encoded)).isTrue();
- assertThat(encoder.matches(WRONG_PASSWORD, encoded)).isFalse();
- }
-
- @ParameterizedTest
- @EnumSource(Argon2.class)
- void argon2WithDifferentTypesShouldWork(Argon2 type) {
- Password4jPasswordEncoder encoder = Password4jPasswordEncoder.argon2(
- 65536, 3, 4, 32, type);
-
- String encoded = encoder.encode(PASSWORD);
- String expectedPrefix = switch (type) {
- case D -> "$argon2d$";
- case I -> "$argon2i$";
- case ID -> "$argon2id$";
- };
-
- assertThat(encoded).startsWith(expectedPrefix);
- assertThat(encoder.matches(PASSWORD, encoded)).isTrue();
- }
-
- // PBKDF2 Tests
- @Test
- void pbkdf2EncoderShouldEncodeAndVerifyPasswords() {
- Password4jPasswordEncoder encoder = Password4jPasswordEncoder.pbkdf2(100000, 32);
-
- String encoded = encoder.encode(PASSWORD);
- assertThat(encoded)
- .isNotNull()
- .isNotEqualTo(PASSWORD);
-
- assertThat(encoder.matches(PASSWORD, encoded)).isTrue();
- assertThat(encoder.matches(WRONG_PASSWORD, encoded)).isFalse();
- }
-
- @Test
- void compressedPbkdf2EncoderShouldEncodeAndVerifyPasswords() {
- Password4jPasswordEncoder encoder = Password4jPasswordEncoder.compressedPbkdf2(100000, 32);
-
- String encoded = encoder.encode(PASSWORD);
- assertThat(encoded)
- .isNotNull()
- .isNotEqualTo(PASSWORD);
-
- assertThat(encoder.matches(PASSWORD, encoded)).isTrue();
- assertThat(encoder.matches(WRONG_PASSWORD, encoded)).isFalse();
- }
-
- @ParameterizedTest
- @CsvSource({
- "50000, 16",
- "100000, 32",
- "200000, 64",
- "500000, 32"
- })
- void pbkdf2WithDifferentParametersShouldWork(int iterations, int keyLength) {
- Password4jPasswordEncoder encoder = Password4jPasswordEncoder.pbkdf2(iterations, keyLength);
-
- String encoded = encoder.encode(PASSWORD);
- assertThat(encoder.matches(PASSWORD, encoded)).isTrue();
- }
-
- // Factory Method Tests
- @Test
- void defaultsForSpringSecurityShouldUseBCrypt() {
- Password4jPasswordEncoder encoder = Password4jPasswordEncoder.defaultsForSpringSecurity();
-
- assertThat(encoder.getAlgorithm()).isEqualTo(Password4jPasswordEncoder.Password4jAlgorithm.BCRYPT);
- assertThat(encoder.getHashingFunction()).isInstanceOf(BcryptFunction.class);
-
- String encoded = encoder.encode(PASSWORD);
- assertThat(encoded).startsWith("$2b$10$"); // Password4j uses $2b$ format
- assertThat(encoder.matches(PASSWORD, encoded)).isTrue();
- }
-
- // Null and Empty Input Tests
- @Test
- void encodeNullPasswordShouldReturnNull() {
- Password4jPasswordEncoder encoder = new Password4jPasswordEncoder();
- assertThat(encoder.encode(null)).isNull();
- }
-
- @Test
- void encodeEmptyPasswordShouldWork() {
- Password4jPasswordEncoder encoder = new Password4jPasswordEncoder();
- String encoded = encoder.encode("");
- assertThat(encoded).isNotNull();
- // AbstractValidatingPasswordEncoder returns false for empty raw passwords
- assertThat(encoder.matches("", encoded)).isFalse();
- }
-
- @Test
- void matchesWithNullOrEmptyParametersShouldReturnFalse() {
- Password4jPasswordEncoder encoder = new Password4jPasswordEncoder();
- String validHash = encoder.encode(PASSWORD);
-
- assertThat(encoder.matches(null, validHash)).isFalse();
- assertThat(encoder.matches("", validHash)).isFalse();
- assertThat(encoder.matches(PASSWORD, null)).isFalse();
- assertThat(encoder.matches(PASSWORD, "")).isFalse();
- assertThat(encoder.matches(null, null)).isFalse();
- assertThat(encoder.matches("", "")).isFalse();
- }
-
- // Password Variety Tests
- @ParameterizedTest
- @ValueSource(strings = {"password", "123456", "P@ssw0rd!", "a very long password with spaces and symbols !@#$%"})
- void shouldHandleVariousPasswordFormats(String password) {
- Password4jPasswordEncoder encoder = Password4jPasswordEncoder.defaultsForSpringSecurity();
-
- String encoded = encoder.encode(password);
- assertThat(encoded).isNotNull();
- assertThat(encoder.matches(password, encoded)).isTrue();
- assertThat(encoder.matches(password + "x", encoded)).isFalse();
- }
-
- @Test
- void shouldHandleUnicodePasswords() {
- Password4jPasswordEncoder encoder = Password4jPasswordEncoder.defaultsForSpringSecurity();
-
- String encoded = encoder.encode(UNICODE_PASSWORD);
- assertThat(encoded).isNotNull();
- assertThat(encoder.matches(UNICODE_PASSWORD, encoded)).isTrue();
- assertThat(encoder.matches("password", encoded)).isFalse();
- }
-
- @Test
- void shouldHandleLongPasswords() {
- Password4jPasswordEncoder encoder = Password4jPasswordEncoder.defaultsForSpringSecurity();
-
- String encoded = encoder.encode(LONG_PASSWORD);
- assertThat(encoded).isNotNull();
- assertThat(encoder.matches(LONG_PASSWORD, encoded)).isTrue();
- }
-
- // Upgrade Encoding Tests
- @Test
- void upgradeEncodingShouldReturnFalse() {
- Password4jPasswordEncoder encoder = new Password4jPasswordEncoder();
- String encoded = encoder.encode(PASSWORD);
-
- // For now, upgradeEncoding should return false
- assertThat(encoder.upgradeEncoding(encoded)).isFalse();
- assertThat(encoder.upgradeEncoding(null)).isFalse();
- assertThat(encoder.upgradeEncoding("")).isFalse();
- }
-
- @ParameterizedTest
- @EnumSource(Password4jPasswordEncoder.Password4jAlgorithm.class)
- void upgradeEncodingShouldReturnFalseForAllAlgorithms(Password4jPasswordEncoder.Password4jAlgorithm algorithm) {
- Password4jPasswordEncoder encoder = new Password4jPasswordEncoder(algorithm);
- String encoded = encoder.encode(PASSWORD);
-
- assertThat(encoder.upgradeEncoding(encoded)).isFalse();
- }
-
- // Custom Hashing Function Tests
- @Test
- void shouldWorkWithCustomHashingFunction() {
- BcryptFunction customFunction = BcryptFunction.getInstance(12);
- Password4jPasswordEncoder encoder = new Password4jPasswordEncoder(customFunction, Password4jPasswordEncoder.Password4jAlgorithm.BCRYPT);
-
- String encoded = encoder.encode(PASSWORD);
- assertThat(encoded).startsWith("$2b$12$"); // Password4j uses $2b$ format
- assertThat(encoder.matches(PASSWORD, encoded)).isTrue();
- }
-
- @Test
- void shouldWorkWithCustomScryptFunction() {
- ScryptFunction customFunction = ScryptFunction.getInstance(32768, 16, 2, 64);
- Password4jPasswordEncoder encoder = new Password4jPasswordEncoder(customFunction, Password4jPasswordEncoder.Password4jAlgorithm.SCRYPT);
-
- String encoded = encoder.encode(PASSWORD);
- assertThat(encoder.matches(PASSWORD, encoded)).isTrue();
- }
-
- @Test
- void shouldWorkWithCustomArgon2Function() {
- Argon2Function customFunction = Argon2Function.getInstance(131072, 4, 8, 64, Argon2.ID);
- Password4jPasswordEncoder encoder = new Password4jPasswordEncoder(customFunction, Password4jPasswordEncoder.Password4jAlgorithm.ARGON2);
-
- String encoded = encoder.encode(PASSWORD);
- assertThat(encoded).startsWith("$argon2id$");
- assertThat(encoder.matches(PASSWORD, encoded)).isTrue();
- }
-
- // Algorithm Coverage Tests
- @Test
- void shouldCreateEncoderForEachAlgorithm() {
- // Test all algorithm types can be instantiated
- for (Password4jPasswordEncoder.Password4jAlgorithm algorithm : Password4jPasswordEncoder.Password4jAlgorithm.values()) {
- Password4jPasswordEncoder encoder = new Password4jPasswordEncoder(algorithm);
- assertThat(encoder.getAlgorithm()).isEqualTo(algorithm);
-
- String encoded = encoder.encode(PASSWORD);
- assertThat(encoded).isNotNull();
- assertThat(encoder.matches(PASSWORD, encoded)).isTrue();
- }
- }
-
- @ParameterizedTest
- @EnumSource(Password4jPasswordEncoder.Password4jAlgorithm.class)
- void allAlgorithmsShouldProduceValidHashes(Password4jPasswordEncoder.Password4jAlgorithm algorithm) {
- Password4jPasswordEncoder encoder = new Password4jPasswordEncoder(algorithm);
-
- String encoded = encoder.encode(PASSWORD);
- assertThat(encoded)
- .isNotNull()
- .isNotEmpty()
- .isNotEqualTo(PASSWORD);
-
- assertThat(encoder.matches(PASSWORD, encoded)).isTrue();
- assertThat(encoder.matches(WRONG_PASSWORD, encoded)).isFalse();
- }
-
- // Security Properties Tests
- @RepeatedTest(10)
- void samePasswordShouldProduceDifferentHashes() {
- Password4jPasswordEncoder encoder = Password4jPasswordEncoder.defaultsForSpringSecurity();
-
- String hash1 = encoder.encode(PASSWORD);
- String hash2 = encoder.encode(PASSWORD);
-
- // Hashes should be different (due to salt)
- assertThat(hash1).isNotEqualTo(hash2);
-
- // But both should verify correctly
- assertThat(encoder.matches(PASSWORD, hash1)).isTrue();
- assertThat(encoder.matches(PASSWORD, hash2)).isTrue();
- }
-
- @Test
- void hashLengthShouldBeConsistent() {
- Password4jPasswordEncoder encoder = Password4jPasswordEncoder.defaultsForSpringSecurity();
-
- String hash1 = encoder.encode("short");
- String hash2 = encoder.encode("this is a much longer password with many characters");
-
- // BCrypt hashes should have consistent length
- assertThat(hash1).hasSize(60); // BCrypt produces 60-character hashes
- assertThat(hash2).hasSize(60);
- }
-
- @Test
- void similarPasswordsShouldProduceCompletelyDifferentHashes() {
- Password4jPasswordEncoder encoder = Password4jPasswordEncoder.defaultsForSpringSecurity();
-
- String hash1 = encoder.encode("password");
- String hash2 = encoder.encode("password1");
- String hash3 = encoder.encode("Password");
-
- assertThat(hash1)
- .isNotEqualTo(hash2)
- .isNotEqualTo(hash3);
- assertThat(hash2).isNotEqualTo(hash3);
-
- // Cross-verification should fail
- assertThat(encoder.matches("password", hash2)).isFalse();
- assertThat(encoder.matches("password1", hash1)).isFalse();
- }
-
-
- // Additional Security and Robustness Tests
- @Test
- void shouldHandleVeryLongPasswords() {
- Password4jPasswordEncoder encoder = Password4jPasswordEncoder.defaultsForSpringSecurity();
- String veryLongPassword = "a".repeat(10000); // 10KB password
-
- String encoded = encoder.encode(veryLongPassword);
- assertThat(encoded).isNotNull();
- assertThat(encoder.matches(veryLongPassword, encoded)).isTrue();
- // Fix: BCrypt truncates passwords longer than 72 bytes, so we need to test with a meaningful difference
- // Test with a shorter difference that's within the 72-byte limit
- String slightlyDifferentPassword = "b" + veryLongPassword.substring(1); // Change first character
- assertThat(encoder.matches(slightlyDifferentPassword, encoded)).isFalse();
- }
-
- @Test
- void shouldHandlePasswordsWithNullBytes() {
- Password4jPasswordEncoder encoder = Password4jPasswordEncoder.defaultsForSpringSecurity();
- String passwordWithNull = "password\u0000test";
-
- String encoded = encoder.encode(passwordWithNull);
- assertThat(encoded).isNotNull();
- assertThat(encoder.matches(passwordWithNull, encoded)).isTrue();
- assertThat(encoder.matches("passwordtest", encoded)).isFalse();
- }
-
- @Test
- void shouldProduceStrongRandomness() {
- Password4jPasswordEncoder encoder = Password4jPasswordEncoder.defaultsForSpringSecurity();
- java.util.Set hashes = new java.util.HashSet<>();
-
- // Generate many hashes of the same password
- for (int i = 0; i < 100; i++) {
- String hash = encoder.encode(PASSWORD);
- assertThat(hashes.add(hash)).isTrue(); // Each hash should be unique
- }
-
- assertThat(hashes).hasSize(100);
- }
-
- @Test
- void shouldResistTimingAttacks() {
- Password4jPasswordEncoder encoder = Password4jPasswordEncoder.defaultsForSpringSecurity();
- String validHash = encoder.encode(PASSWORD);
-
- // Measure time for correct password
- long startTime = System.nanoTime();
- boolean result1 = encoder.matches(PASSWORD, validHash);
- long correctTime = System.nanoTime() - startTime;
-
- // Measure time for wrong password of same length
- startTime = System.nanoTime();
- boolean result2 = encoder.matches("passwore", validHash); // Same length, different content
- long wrongTime = System.nanoTime() - startTime;
-
- assertThat(result1).isTrue();
- assertThat(result2).isFalse();
-
- // Times should be relatively close (within 10x factor for timing attack resistance)
- double ratio = Math.max(correctTime, wrongTime) / (double) Math.min(correctTime, wrongTime);
- assertThat(ratio).isLessThan(10.0);
- }
-
-
- @Test
- void scryptShouldHandleEdgeCaseParameters() {
- // Test with minimum viable parameters
- Password4jPasswordEncoder encoder = Password4jPasswordEncoder.scrypt(2, 1, 1, 16);
-
- String encoded = encoder.encode(PASSWORD);
- assertThat(encoder.matches(PASSWORD, encoded)).isTrue();
- }
-
- @Test
- void argon2ShouldWorkWithDifferentMemorySizes() {
- // Test with various memory configurations
- int[] memorySizes = {1024, 4096, 16384, 65536};
-
- for (int memory : memorySizes) {
- Password4jPasswordEncoder encoder = Password4jPasswordEncoder.argon2(memory, 2, 1, 32, Argon2.ID);
- String encoded = encoder.encode(PASSWORD);
- assertThat(encoder.matches(PASSWORD, encoded)).isTrue();
- }
- }
-
- @Test
- void pbkdf2ShouldWorkWithDifferentHashAlgorithms() {
- // Test that the implementation handles different internal configurations
- Password4jPasswordEncoder encoder1 = Password4jPasswordEncoder.pbkdf2(50000, 16);
- Password4jPasswordEncoder encoder2 = Password4jPasswordEncoder.pbkdf2(100000, 32);
- Password4jPasswordEncoder encoder3 = Password4jPasswordEncoder.pbkdf2(200000, 64);
-
- String hash1 = encoder1.encode(PASSWORD);
- String hash2 = encoder2.encode(PASSWORD);
- String hash3 = encoder3.encode(PASSWORD);
-
- assertThat(encoder1.matches(PASSWORD, hash1)).isTrue();
- assertThat(encoder2.matches(PASSWORD, hash2)).isTrue();
- assertThat(encoder3.matches(PASSWORD, hash3)).isTrue();
-
- // Hashes should be different due to different parameters
- assertThat(hash1).isNotEqualTo(hash2);
- assertThat(hash2).isNotEqualTo(hash3);
- }
-
- // Cross-Algorithm Verification Tests
- @Test
- void differentAlgorithmsShouldNotCrossVerify() {
- Password4jPasswordEncoder bcryptEncoder = Password4jPasswordEncoder.bcrypt(10);
- Password4jPasswordEncoder scryptEncoder = Password4jPasswordEncoder.scrypt(16384, 8, 1, 32);
- Password4jPasswordEncoder argon2Encoder = Password4jPasswordEncoder.argon2(65536, 3, 4, 32, Argon2.ID);
-
- String bcryptHash = bcryptEncoder.encode(PASSWORD);
- String scryptHash = scryptEncoder.encode(PASSWORD);
- String argon2Hash = argon2Encoder.encode(PASSWORD);
-
- // Each encoder should only verify its own hashes
- assertThat(bcryptEncoder.matches(PASSWORD, bcryptHash)).isTrue();
- assertThat(bcryptEncoder.matches(PASSWORD, scryptHash)).isFalse();
- assertThat(bcryptEncoder.matches(PASSWORD, argon2Hash)).isFalse();
-
- assertThat(scryptEncoder.matches(PASSWORD, scryptHash)).isTrue();
- assertThat(scryptEncoder.matches(PASSWORD, bcryptHash)).isFalse();
- assertThat(scryptEncoder.matches(PASSWORD, argon2Hash)).isFalse();
-
- assertThat(argon2Encoder.matches(PASSWORD, argon2Hash)).isTrue();
- assertThat(argon2Encoder.matches(PASSWORD, bcryptHash)).isFalse();
- assertThat(argon2Encoder.matches(PASSWORD, scryptHash)).isFalse();
- }
-
-
- @Test
- void encodingShouldCompleteInReasonableTime() {
- Password4jPasswordEncoder encoder = Password4jPasswordEncoder.defaultsForSpringSecurity();
-
- long startTime = System.currentTimeMillis();
- String encoded = encoder.encode(PASSWORD);
- long duration = System.currentTimeMillis() - startTime;
-
- assertThat(encoded).isNotNull();
- assertThat(duration).isLessThan(5000); // Should complete within 5 seconds
- }
-
- // Compatibility and Integration Tests
- @Test
- void shouldBeCompatibleWithSpringSecurityConventions() {
- Password4jPasswordEncoder encoder = Password4jPasswordEncoder.defaultsForSpringSecurity();
-
- // Test common Spring Security patterns
- assertThat(encoder.encode(null)).isNull();
- assertThat(encoder.matches(null, "hash")).isFalse();
- assertThat(encoder.matches("password", null)).isFalse();
- assertThat(encoder.upgradeEncoding("anyhash")).isFalse();
-
- // Test that it follows AbstractValidatingPasswordEncoder contract
- assertThat(encoder.matches("", "")).isFalse();
- assertThat(encoder.upgradeEncoding("")).isFalse();
- }
-
- @Test
- void factoryMethodsShouldCreateCorrectInstances() {
- // Verify all factory methods create properly configured instances
- Password4jPasswordEncoder bcrypt = Password4jPasswordEncoder.bcrypt(12);
- assertThat(bcrypt.getAlgorithm()).isEqualTo(Password4jPasswordEncoder.Password4jAlgorithm.BCRYPT);
- assertThat(bcrypt.getHashingFunction()).isInstanceOf(BcryptFunction.class);
-
- Password4jPasswordEncoder scrypt = Password4jPasswordEncoder.scrypt(32768, 8, 1, 32);
- assertThat(scrypt.getAlgorithm()).isEqualTo(Password4jPasswordEncoder.Password4jAlgorithm.SCRYPT);
- assertThat(scrypt.getHashingFunction()).isInstanceOf(ScryptFunction.class);
-
- Password4jPasswordEncoder argon2 = Password4jPasswordEncoder.argon2(65536, 3, 4, 32, Argon2.ID);
- assertThat(argon2.getAlgorithm()).isEqualTo(Password4jPasswordEncoder.Password4jAlgorithm.ARGON2);
- assertThat(argon2.getHashingFunction()).isInstanceOf(Argon2Function.class);
-
- Password4jPasswordEncoder pbkdf2 = Password4jPasswordEncoder.pbkdf2(100000, 32);
- assertThat(pbkdf2.getAlgorithm()).isEqualTo(Password4jPasswordEncoder.Password4jAlgorithm.PBKDF2);
- assertThat(pbkdf2.getHashingFunction()).isInstanceOf(CompressedPBKDF2Function.class);
-
- Password4jPasswordEncoder compressedPbkdf2 = Password4jPasswordEncoder.compressedPbkdf2(100000, 32);
- assertThat(compressedPbkdf2.getAlgorithm()).isEqualTo(Password4jPasswordEncoder.Password4jAlgorithm.COMPRESSED_PBKDF2);
- assertThat(compressedPbkdf2.getHashingFunction()).isInstanceOf(CompressedPBKDF2Function.class);
- }
}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 63f14d6893..9558e11acc 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -13,6 +13,7 @@ org-jetbrains-kotlinx = "1.10.2"
org-mockito = "5.17.0"
org-opensaml5 = "5.1.6"
org-springframework = "7.0.0-M9"
+com-password4j = "1.8.2"
[libraries]
ch-qos-logback-logback-classic = "ch.qos.logback:logback-classic:1.5.18"
@@ -101,6 +102,7 @@ org-instancio-instancio-junit = "org.instancio:instancio-junit:3.7.1"
spring-nullability = 'io.spring.nullability:io.spring.nullability.gradle.plugin:0.0.4'
webauthn4j-core = 'com.webauthn4j:webauthn4j-core:0.29.6.RELEASE'
+com-password4j-password4j = { module = "com.password4j:password4j", version.ref = "com-password4j" }
[plugins]