Browse Source
Closes gh-17706 Signed-off-by: M.Bozorgmehr <mehrdad.bozorgmehr@gmail.com> Add Password4jPasswordEncoder for enhanced password hashing support Signed-off-by: M.Bozorgmehr <m.bozorgmehr@emofid.com> Signed-off-by: M.Bozorgmehr <mehrdad.bozorgmehr@gmail.com> Add Password4jPasswordEncoder for enhanced password hashing support Signed-off-by: M.Bozorgmehr <mehrdad.bozorgmehr@gmail.com> Signed-off-by: Mehrdad <mehrdad.bozorgmehr@gmail.com> Signed-off-by: M.Bozorgmehr <mehrdad.bozorgmehr@gmail.com>pull/17825/head
7 changed files with 1041 additions and 1 deletions
@ -0,0 +1,249 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2004-present 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.password4j; |
||||||
|
|
||||||
|
import com.password4j.*; |
||||||
|
import com.password4j.types.Argon2; |
||||||
|
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. |
||||||
|
* |
||||||
|
* <p>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.</p> |
||||||
|
* |
||||||
|
* <p>This implementation is thread-safe and can be shared across multiple threads.</p> |
||||||
|
* |
||||||
|
* @author Mehrdad Bozorgmehr |
||||||
|
* @since 6.5 |
||||||
|
*/ |
||||||
|
public class Password4jPasswordEncoder extends AbstractValidatingPasswordEncoder { |
||||||
|
|
||||||
|
private final Log logger = LogFactory.getLog(getClass()); |
||||||
|
|
||||||
|
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. |
||||||
|
* |
||||||
|
* @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. |
||||||
|
* |
||||||
|
* @param hashingFunction the custom hashing function to use |
||||||
|
* @param algorithm the password hashing algorithm type |
||||||
|
*/ |
||||||
|
public Password4jPasswordEncoder(HashingFunction hashingFunction, Password4jAlgorithm algorithm) { |
||||||
|
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 |
||||||
|
protected String encodeNonNullPassword(String rawPassword) { |
||||||
|
try { |
||||||
|
Hash hash = Password.hash(rawPassword).with(this.hashingFunction); |
||||||
|
return hash.getResult(); |
||||||
|
} catch (Exception ex) { |
||||||
|
throw new IllegalStateException("Failed to encode password using Password4j", ex); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected boolean matchesNonNull(String rawPassword, String encodedPassword) { |
||||||
|
try { |
||||||
|
// Use the specific hashing function for verification
|
||||||
|
return Password.check(rawPassword, encodedPassword).with(this.hashingFunction); |
||||||
|
} catch (Exception ex) { |
||||||
|
this.logger.warn("Password verification failed for encoded password: " + encodedPassword, ex); |
||||||
|
return false; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected boolean upgradeEncodingNonNull(String encodedPassword) { |
||||||
|
// Password4j handles upgrade detection internally for most algorithms
|
||||||
|
// For now, we'll return false to maintain existing behavior
|
||||||
|
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; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,21 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2004-present 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. |
||||||
|
*/ |
||||||
|
|
||||||
|
|
||||||
|
@NullMarked |
||||||
|
package org.springframework.security.crypto.password4j; |
||||||
|
|
||||||
|
import org.jspecify.annotations.NullMarked; |
||||||
@ -0,0 +1,596 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2004-present 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.password4j; |
||||||
|
|
||||||
|
import com.password4j.Argon2Function; |
||||||
|
import com.password4j.BcryptFunction; |
||||||
|
import com.password4j.CompressedPBKDF2Function; |
||||||
|
import com.password4j.ScryptFunction; |
||||||
|
import com.password4j.types.Argon2; |
||||||
|
import org.junit.jupiter.api.RepeatedTest; |
||||||
|
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; |
||||||
|
|
||||||
|
/** |
||||||
|
* Tests for {@link Password4jPasswordEncoder}. |
||||||
|
* |
||||||
|
* @author Mehrdad Bozorgmehr |
||||||
|
*/ |
||||||
|
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); |
||||||
|
|
||||||
|
// 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<String> 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); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,160 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2004-present 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.password4j; |
||||||
|
|
||||||
|
import com.password4j.AlgorithmFinder; |
||||||
|
import com.password4j.Argon2Function; |
||||||
|
import com.password4j.BcryptFunction; |
||||||
|
import com.password4j.CompressedPBKDF2Function; |
||||||
|
import com.password4j.ScryptFunction; |
||||||
|
import com.password4j.types.Argon2; |
||||||
|
import org.junit.jupiter.api.Test; |
||||||
|
|
||||||
|
import org.springframework.security.crypto.argon2.Argon2PasswordEncoder; |
||||||
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; |
||||||
|
import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder; |
||||||
|
import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder; |
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat; |
||||||
|
|
||||||
|
/** |
||||||
|
* Tests compatibility between existing Spring Security password encoders and |
||||||
|
* {@link Password4jPasswordEncoder}. |
||||||
|
* |
||||||
|
* @author Mehrdad Bozorgmehr |
||||||
|
*/ |
||||||
|
class PasswordCompatibilityTests { |
||||||
|
|
||||||
|
private static final String PASSWORD = "password"; |
||||||
|
|
||||||
|
// BCrypt Compatibility Tests
|
||||||
|
@Test |
||||||
|
void bcryptEncodedWithSpringSecurityShouldMatchWithPassword4j() { |
||||||
|
BCryptPasswordEncoder springEncoder = new BCryptPasswordEncoder(10); |
||||||
|
Password4jPasswordEncoder password4jEncoder = new Password4jPasswordEncoder(BcryptFunction.getInstance(10)); |
||||||
|
|
||||||
|
String encodedBySpring = springEncoder.encode(PASSWORD); |
||||||
|
boolean matchedByPassword4j = password4jEncoder.matches(PASSWORD, encodedBySpring); |
||||||
|
|
||||||
|
assertThat(matchedByPassword4j).isTrue(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
void bcryptEncodedWithPassword4jShouldMatchWithSpringSecurity() { |
||||||
|
BCryptPasswordEncoder springEncoder = new BCryptPasswordEncoder(10); |
||||||
|
Password4jPasswordEncoder password4jEncoder = new Password4jPasswordEncoder(BcryptFunction.getInstance(10)); |
||||||
|
|
||||||
|
String encodedByPassword4j = password4jEncoder.encode(PASSWORD); |
||||||
|
boolean matchedBySpring = springEncoder.matches(PASSWORD, encodedByPassword4j); |
||||||
|
|
||||||
|
assertThat(matchedBySpring).isTrue(); |
||||||
|
} |
||||||
|
|
||||||
|
// SCrypt Compatibility Tests
|
||||||
|
@Test |
||||||
|
void scryptEncodedWithSpringSecurityShouldMatchWithPassword4j() { |
||||||
|
SCryptPasswordEncoder springEncoder = new SCryptPasswordEncoder(16384, 8, 1, 32, 64); |
||||||
|
Password4jPasswordEncoder password4jEncoder = new Password4jPasswordEncoder( |
||||||
|
ScryptFunction.getInstance(16384, 8, 1, 32)); |
||||||
|
|
||||||
|
String encodedBySpring = springEncoder.encode(PASSWORD); |
||||||
|
boolean matchedByPassword4j = password4jEncoder.matches(PASSWORD, encodedBySpring); |
||||||
|
|
||||||
|
assertThat(matchedByPassword4j).isTrue(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
void scryptEncodedWithPassword4jShouldMatchWithSpringSecurity() { |
||||||
|
SCryptPasswordEncoder springEncoder = new SCryptPasswordEncoder(16384, 8, 1, 32, 64); |
||||||
|
Password4jPasswordEncoder password4jEncoder = new Password4jPasswordEncoder( |
||||||
|
ScryptFunction.getInstance(16384, 8, 1, 32)); |
||||||
|
|
||||||
|
String encodedByPassword4j = password4jEncoder.encode(PASSWORD); |
||||||
|
boolean matchedBySpring = springEncoder.matches(PASSWORD, encodedByPassword4j); |
||||||
|
|
||||||
|
assertThat(matchedBySpring).isTrue(); |
||||||
|
} |
||||||
|
|
||||||
|
// Argon2 Compatibility Tests
|
||||||
|
@Test |
||||||
|
void argon2EncodedWithSpringSecurityShouldMatchWithPassword4j() { |
||||||
|
Argon2PasswordEncoder springEncoder = new Argon2PasswordEncoder(16, 32, 1, 65536, 3); |
||||||
|
Password4jPasswordEncoder password4jEncoder = new Password4jPasswordEncoder( |
||||||
|
Argon2Function.getInstance(65536, 3, 1, 32, Argon2.ID)); |
||||||
|
|
||||||
|
String encodedBySpring = springEncoder.encode(PASSWORD); |
||||||
|
boolean matchedByPassword4j = password4jEncoder.matches(PASSWORD, encodedBySpring); |
||||||
|
|
||||||
|
assertThat(matchedByPassword4j).isTrue(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
void argon2EncodedWithPassword4jShouldMatchWithSpringSecurity() { |
||||||
|
Argon2PasswordEncoder springEncoder = new Argon2PasswordEncoder(16, 32, 1, 65536, 3); |
||||||
|
Password4jPasswordEncoder password4jEncoder = new Password4jPasswordEncoder( |
||||||
|
Argon2Function.getInstance(65536, 3, 1, 32, Argon2.ID)); |
||||||
|
|
||||||
|
String encodedByPassword4j = password4jEncoder.encode(PASSWORD); |
||||||
|
boolean matchedBySpring = springEncoder.matches(PASSWORD, encodedByPassword4j); |
||||||
|
|
||||||
|
assertThat(matchedBySpring).isTrue(); |
||||||
|
} |
||||||
|
|
||||||
|
// PBKDF2 Compatibility Tests - Note: Different format implementations
|
||||||
|
@Test |
||||||
|
void pbkdf2BasicFunctionalityTest() { |
||||||
|
// Test that both encoders work independently with their own formats
|
||||||
|
// Spring Security PBKDF2
|
||||||
|
Pbkdf2PasswordEncoder springEncoder = new Pbkdf2PasswordEncoder("", 16, 100000, |
||||||
|
Pbkdf2PasswordEncoder.SecretKeyFactoryAlgorithm.PBKDF2WithHmacSHA256); |
||||||
|
String springEncoded = springEncoder.encode(PASSWORD); |
||||||
|
assertThat(springEncoder.matches(PASSWORD, springEncoded)).isTrue(); |
||||||
|
|
||||||
|
// Password4j PBKDF2
|
||||||
|
Password4jPasswordEncoder password4jEncoder = new Password4jPasswordEncoder( |
||||||
|
CompressedPBKDF2Function.getInstance("SHA256", 100000, 32)); |
||||||
|
String password4jEncoded = password4jEncoder.encode(PASSWORD); |
||||||
|
assertThat(password4jEncoder.matches(PASSWORD, password4jEncoded)).isTrue(); |
||||||
|
|
||||||
|
// Note: These encoders use different hash formats and are not cross-compatible
|
||||||
|
// This is expected behavior due to different implementation standards
|
||||||
|
} |
||||||
|
|
||||||
|
// Cross-Algorithm Tests (should fail)
|
||||||
|
@Test |
||||||
|
void bcryptEncodedPasswordShouldNotMatchArgon2Encoder() { |
||||||
|
Password4jPasswordEncoder bcryptEncoder = new Password4jPasswordEncoder(BcryptFunction.getInstance(10)); |
||||||
|
Password4jPasswordEncoder argon2Encoder = new Password4jPasswordEncoder(AlgorithmFinder.getArgon2Instance()); |
||||||
|
|
||||||
|
String bcryptEncoded = bcryptEncoder.encode(PASSWORD); |
||||||
|
boolean matchedByArgon2 = argon2Encoder.matches(PASSWORD, bcryptEncoded); |
||||||
|
|
||||||
|
assertThat(matchedByArgon2).isFalse(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
void argon2EncodedPasswordShouldNotMatchScryptEncoder() { |
||||||
|
Password4jPasswordEncoder argon2Encoder = new Password4jPasswordEncoder(AlgorithmFinder.getArgon2Instance()); |
||||||
|
Password4jPasswordEncoder scryptEncoder = new Password4jPasswordEncoder(AlgorithmFinder.getScryptInstance()); |
||||||
|
|
||||||
|
String argon2Encoded = argon2Encoder.encode(PASSWORD); |
||||||
|
boolean matchedByScrypt = scryptEncoder.matches(PASSWORD, argon2Encoded); |
||||||
|
|
||||||
|
assertThat(matchedByScrypt).isFalse(); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
Loading…
Reference in new issue