Browse Source
Closes gh-17706 Signed-off-by: Mehrdad <mehrdad.bozorgmehr@gmail.com> Signed-off-by: M.Bozorgmehr <mehrdad.bozorgmehr@gmail.com>pull/17825/head
5 changed files with 693 additions and 3 deletions
@ -0,0 +1,159 @@
@@ -0,0 +1,159 @@
|
||||
/* |
||||
* 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 java.security.SecureRandom; |
||||
import java.util.Base64; |
||||
|
||||
import com.password4j.AlgorithmFinder; |
||||
import com.password4j.BalloonHashingFunction; |
||||
import com.password4j.Hash; |
||||
import com.password4j.Password; |
||||
|
||||
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 with Balloon hashing algorithm. |
||||
* |
||||
* <p> |
||||
* Balloon hashing is a memory-hard password hashing algorithm designed to be resistant to |
||||
* both time-memory trade-off attacks and side-channel attacks. This implementation |
||||
* handles the salt management explicitly since Password4j's Balloon hashing |
||||
* implementation does not include the salt in the output hash. |
||||
* </p> |
||||
* |
||||
* <p> |
||||
* The encoded password format is: {salt}:{hash} where both salt and hash are Base64 |
||||
* encoded. |
||||
* </p> |
||||
* |
||||
* <p> |
||||
* This implementation is thread-safe and can be shared across multiple threads. |
||||
* </p> |
||||
* |
||||
* <p> |
||||
* <strong>Usage Examples:</strong> |
||||
* </p> |
||||
* <pre>{@code |
||||
* // Using default Balloon hashing settings (recommended)
|
||||
* PasswordEncoder encoder = new BalloonHashingPassword4jPasswordEncoder(); |
||||
* |
||||
* // Using custom Balloon hashing function
|
||||
* PasswordEncoder customEncoder = new BalloonHashingPassword4jPasswordEncoder( |
||||
* BalloonHashingFunction.getInstance(1024, 3, 4, "SHA-256")); |
||||
* }</pre> |
||||
* |
||||
* @author Mehrdad Bozorgmehr |
||||
* @since 7.0 |
||||
* @see BalloonHashingFunction |
||||
* @see AlgorithmFinder#getBalloonHashingInstance() |
||||
*/ |
||||
public class BalloonHashingPassword4jPasswordEncoder extends AbstractValidatingPasswordEncoder { |
||||
|
||||
private static final String DELIMITER = ":"; |
||||
|
||||
private static final int DEFAULT_SALT_LENGTH = 32; |
||||
|
||||
private final BalloonHashingFunction balloonHashingFunction; |
||||
|
||||
private final SecureRandom secureRandom; |
||||
|
||||
private final int saltLength; |
||||
|
||||
/** |
||||
* Constructs a Balloon hashing password encoder using the default Balloon hashing |
||||
* configuration from Password4j's AlgorithmFinder. |
||||
*/ |
||||
public BalloonHashingPassword4jPasswordEncoder() { |
||||
this(AlgorithmFinder.getBalloonHashingInstance()); |
||||
} |
||||
|
||||
/** |
||||
* Constructs a Balloon hashing password encoder with a custom Balloon hashing |
||||
* function. |
||||
* @param balloonHashingFunction the Balloon hashing function to use for encoding |
||||
* passwords, must not be null |
||||
* @throws IllegalArgumentException if balloonHashingFunction is null |
||||
*/ |
||||
public BalloonHashingPassword4jPasswordEncoder(BalloonHashingFunction balloonHashingFunction) { |
||||
this(balloonHashingFunction, DEFAULT_SALT_LENGTH); |
||||
} |
||||
|
||||
/** |
||||
* Constructs a Balloon hashing password encoder with a custom Balloon hashing |
||||
* function and salt length. |
||||
* @param balloonHashingFunction the Balloon hashing function to use for encoding |
||||
* passwords, must not be null |
||||
* @param saltLength the length of the salt in bytes, must be positive |
||||
* @throws IllegalArgumentException if balloonHashingFunction is null or saltLength is |
||||
* not positive |
||||
*/ |
||||
public BalloonHashingPassword4jPasswordEncoder(BalloonHashingFunction balloonHashingFunction, int saltLength) { |
||||
Assert.notNull(balloonHashingFunction, "balloonHashingFunction cannot be null"); |
||||
Assert.isTrue(saltLength > 0, "saltLength must be positive"); |
||||
this.balloonHashingFunction = balloonHashingFunction; |
||||
this.saltLength = saltLength; |
||||
this.secureRandom = new SecureRandom(); |
||||
} |
||||
|
||||
@Override |
||||
protected String encodeNonNullPassword(String rawPassword) { |
||||
byte[] salt = new byte[this.saltLength]; |
||||
this.secureRandom.nextBytes(salt); |
||||
|
||||
Hash hash = Password.hash(rawPassword).addSalt(salt).with(this.balloonHashingFunction); |
||||
String encodedSalt = Base64.getEncoder().encodeToString(salt); |
||||
String encodedHash = hash.getResult(); |
||||
|
||||
return encodedSalt + DELIMITER + encodedHash; |
||||
} |
||||
|
||||
@Override |
||||
protected boolean matchesNonNull(String rawPassword, String encodedPassword) { |
||||
if (!encodedPassword.contains(DELIMITER)) { |
||||
return false; |
||||
} |
||||
|
||||
String[] parts = encodedPassword.split(DELIMITER, 2); |
||||
if (parts.length != 2) { |
||||
return false; |
||||
} |
||||
|
||||
try { |
||||
byte[] salt = Base64.getDecoder().decode(parts[0]); |
||||
String expectedHash = parts[1]; |
||||
|
||||
Hash hash = Password.hash(rawPassword).addSalt(salt).with(this.balloonHashingFunction); |
||||
return expectedHash.equals(hash.getResult()); |
||||
} |
||||
catch (IllegalArgumentException ex) { |
||||
// Invalid Base64 encoding
|
||||
return false; |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
protected boolean upgradeEncodingNonNull(String encodedPassword) { |
||||
// For now, we'll return false to maintain existing behavior
|
||||
// This could be enhanced in the future to check if the encoding parameters
|
||||
// match the current configuration
|
||||
return false; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,157 @@
@@ -0,0 +1,157 @@
|
||||
/* |
||||
* 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 java.security.SecureRandom; |
||||
import java.util.Base64; |
||||
|
||||
import com.password4j.AlgorithmFinder; |
||||
import com.password4j.Hash; |
||||
import com.password4j.PBKDF2Function; |
||||
import com.password4j.Password; |
||||
|
||||
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 with PBKDF2 hashing algorithm. |
||||
* |
||||
* <p> |
||||
* PBKDF2 is a key derivation function designed to be computationally expensive to thwart |
||||
* dictionary and brute force attacks. This implementation handles the salt management |
||||
* explicitly since Password4j's PBKDF2 implementation does not include the salt in the |
||||
* output hash. |
||||
* </p> |
||||
* |
||||
* <p> |
||||
* The encoded password format is: {salt}:{hash} where both salt and hash are Base64 |
||||
* encoded. |
||||
* </p> |
||||
* |
||||
* <p> |
||||
* This implementation is thread-safe and can be shared across multiple threads. |
||||
* </p> |
||||
* |
||||
* <p> |
||||
* <strong>Usage Examples:</strong> |
||||
* </p> |
||||
* <pre>{@code |
||||
* // Using default PBKDF2 settings (recommended)
|
||||
* PasswordEncoder encoder = new Pbkdf2Password4jPasswordEncoder(); |
||||
* |
||||
* // Using custom PBKDF2 function
|
||||
* PasswordEncoder customEncoder = new Pbkdf2Password4jPasswordEncoder( |
||||
* PBKDF2Function.getInstance(Algorithm.HMAC_SHA256, 100000, 256)); |
||||
* }</pre> |
||||
* |
||||
* @author Mehrdad Bozorgmehr |
||||
* @since 7.0 |
||||
* @see PBKDF2Function |
||||
* @see AlgorithmFinder#getPBKDF2Instance() |
||||
*/ |
||||
public class Pbkdf2Password4jPasswordEncoder extends AbstractValidatingPasswordEncoder { |
||||
|
||||
private static final String DELIMITER = ":"; |
||||
|
||||
private static final int DEFAULT_SALT_LENGTH = 32; |
||||
|
||||
private final PBKDF2Function pbkdf2Function; |
||||
|
||||
private final SecureRandom secureRandom; |
||||
|
||||
private final int saltLength; |
||||
|
||||
/** |
||||
* Constructs a PBKDF2 password encoder using the default PBKDF2 configuration from |
||||
* Password4j's AlgorithmFinder. |
||||
*/ |
||||
public Pbkdf2Password4jPasswordEncoder() { |
||||
this(AlgorithmFinder.getPBKDF2Instance()); |
||||
} |
||||
|
||||
/** |
||||
* Constructs a PBKDF2 password encoder with a custom PBKDF2 function. |
||||
* @param pbkdf2Function the PBKDF2 function to use for encoding passwords, must not |
||||
* be null |
||||
* @throws IllegalArgumentException if pbkdf2Function is null |
||||
*/ |
||||
public Pbkdf2Password4jPasswordEncoder(PBKDF2Function pbkdf2Function) { |
||||
this(pbkdf2Function, DEFAULT_SALT_LENGTH); |
||||
} |
||||
|
||||
/** |
||||
* Constructs a PBKDF2 password encoder with a custom PBKDF2 function and salt length. |
||||
* @param pbkdf2Function the PBKDF2 function to use for encoding passwords, must not |
||||
* be null |
||||
* @param saltLength the length of the salt in bytes, must be positive |
||||
* @throws IllegalArgumentException if pbkdf2Function is null or saltLength is not |
||||
* positive |
||||
*/ |
||||
public Pbkdf2Password4jPasswordEncoder(PBKDF2Function pbkdf2Function, int saltLength) { |
||||
Assert.notNull(pbkdf2Function, "pbkdf2Function cannot be null"); |
||||
Assert.isTrue(saltLength > 0, "saltLength must be positive"); |
||||
this.pbkdf2Function = pbkdf2Function; |
||||
this.saltLength = saltLength; |
||||
this.secureRandom = new SecureRandom(); |
||||
} |
||||
|
||||
@Override |
||||
protected String encodeNonNullPassword(String rawPassword) { |
||||
byte[] salt = new byte[this.saltLength]; |
||||
this.secureRandom.nextBytes(salt); |
||||
|
||||
Hash hash = Password.hash(rawPassword).addSalt(salt).with(this.pbkdf2Function); |
||||
String encodedSalt = Base64.getEncoder().encodeToString(salt); |
||||
String encodedHash = hash.getResult(); |
||||
|
||||
return encodedSalt + DELIMITER + encodedHash; |
||||
} |
||||
|
||||
@Override |
||||
protected boolean matchesNonNull(String rawPassword, String encodedPassword) { |
||||
if (!encodedPassword.contains(DELIMITER)) { |
||||
return false; |
||||
} |
||||
|
||||
String[] parts = encodedPassword.split(DELIMITER, 2); |
||||
if (parts.length != 2) { |
||||
return false; |
||||
} |
||||
|
||||
try { |
||||
byte[] salt = Base64.getDecoder().decode(parts[0]); |
||||
String expectedHash = parts[1]; |
||||
|
||||
Hash hash = Password.hash(rawPassword).addSalt(salt).with(this.pbkdf2Function); |
||||
return expectedHash.equals(hash.getResult()); |
||||
} |
||||
catch (IllegalArgumentException ex) { |
||||
// Invalid Base64 encoding
|
||||
return false; |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
protected boolean upgradeEncodingNonNull(String encodedPassword) { |
||||
// For now, we'll return false to maintain existing behavior
|
||||
// This could be enhanced in the future to check if the encoding parameters
|
||||
// match the current configuration
|
||||
return false; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,170 @@
@@ -0,0 +1,170 @@
|
||||
/* |
||||
* 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.BalloonHashingFunction; |
||||
import org.junit.jupiter.api.Test; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; |
||||
|
||||
/** |
||||
* Tests for {@link BalloonHashingPassword4jPasswordEncoder}. |
||||
* |
||||
* @author Mehrdad Bozorgmehr |
||||
*/ |
||||
class BalloonHashingPassword4jPasswordEncoderTests { |
||||
|
||||
private static final String PASSWORD = "password"; |
||||
|
||||
private static final String DIFFERENT_PASSWORD = "differentpassword"; |
||||
|
||||
@Test |
||||
void constructorWithNullFunctionShouldThrowException() { |
||||
assertThatIllegalArgumentException().isThrownBy(() -> new BalloonHashingPassword4jPasswordEncoder(null)) |
||||
.withMessage("balloonHashingFunction cannot be null"); |
||||
} |
||||
|
||||
@Test |
||||
void constructorWithInvalidSaltLengthShouldThrowException() { |
||||
BalloonHashingFunction function = AlgorithmFinder.getBalloonHashingInstance(); |
||||
assertThatIllegalArgumentException().isThrownBy(() -> new BalloonHashingPassword4jPasswordEncoder(function, 0)) |
||||
.withMessage("saltLength must be positive"); |
||||
assertThatIllegalArgumentException().isThrownBy(() -> new BalloonHashingPassword4jPasswordEncoder(function, -1)) |
||||
.withMessage("saltLength must be positive"); |
||||
} |
||||
|
||||
@Test |
||||
void defaultConstructorShouldWork() { |
||||
BalloonHashingPassword4jPasswordEncoder encoder = new BalloonHashingPassword4jPasswordEncoder(); |
||||
|
||||
String encoded = encoder.encode(PASSWORD); |
||||
|
||||
assertThat(encoded).isNotNull().isNotEqualTo(PASSWORD); |
||||
assertThat(encoded).contains(":"); |
||||
assertThat(encoder.matches(PASSWORD, encoded)).isTrue(); |
||||
} |
||||
|
||||
@Test |
||||
void customFunctionConstructorShouldWork() { |
||||
BalloonHashingFunction customFunction = BalloonHashingFunction.getInstance("SHA-256", 512, 2, 3); |
||||
BalloonHashingPassword4jPasswordEncoder encoder = new BalloonHashingPassword4jPasswordEncoder(customFunction); |
||||
|
||||
String encoded = encoder.encode(PASSWORD); |
||||
|
||||
assertThat(encoded).isNotNull().isNotEqualTo(PASSWORD); |
||||
assertThat(encoded).contains(":"); |
||||
assertThat(encoder.matches(PASSWORD, encoded)).isTrue(); |
||||
} |
||||
|
||||
@Test |
||||
void customSaltLengthConstructorShouldWork() { |
||||
BalloonHashingFunction function = AlgorithmFinder.getBalloonHashingInstance(); |
||||
BalloonHashingPassword4jPasswordEncoder encoder = new BalloonHashingPassword4jPasswordEncoder(function, 16); |
||||
|
||||
String encoded = encoder.encode(PASSWORD); |
||||
|
||||
assertThat(encoded).isNotNull().isNotEqualTo(PASSWORD); |
||||
assertThat(encoded).contains(":"); |
||||
assertThat(encoder.matches(PASSWORD, encoded)).isTrue(); |
||||
} |
||||
|
||||
@Test |
||||
void encodeShouldIncludeSaltInOutput() { |
||||
BalloonHashingPassword4jPasswordEncoder encoder = new BalloonHashingPassword4jPasswordEncoder(); |
||||
|
||||
String encoded = encoder.encode(PASSWORD); |
||||
|
||||
assertThat(encoded).contains(":"); |
||||
String[] parts = encoded.split(":"); |
||||
assertThat(parts).hasSize(2); |
||||
assertThat(parts[0]).isNotEmpty(); // salt part
|
||||
assertThat(parts[1]).isNotEmpty(); // hash part
|
||||
} |
||||
|
||||
@Test |
||||
void matchesShouldReturnTrueForCorrectPassword() { |
||||
BalloonHashingPassword4jPasswordEncoder encoder = new BalloonHashingPassword4jPasswordEncoder(); |
||||
|
||||
String encoded = encoder.encode(PASSWORD); |
||||
boolean matches = encoder.matches(PASSWORD, encoded); |
||||
|
||||
assertThat(matches).isTrue(); |
||||
} |
||||
|
||||
@Test |
||||
void matchesShouldReturnFalseForIncorrectPassword() { |
||||
BalloonHashingPassword4jPasswordEncoder encoder = new BalloonHashingPassword4jPasswordEncoder(); |
||||
|
||||
String encoded = encoder.encode(PASSWORD); |
||||
boolean matches = encoder.matches(DIFFERENT_PASSWORD, encoded); |
||||
|
||||
assertThat(matches).isFalse(); |
||||
} |
||||
|
||||
@Test |
||||
void matchesShouldReturnFalseForMalformedEncodedPassword() { |
||||
BalloonHashingPassword4jPasswordEncoder encoder = new BalloonHashingPassword4jPasswordEncoder(); |
||||
|
||||
assertThat(encoder.matches(PASSWORD, "malformed")).isFalse(); |
||||
assertThat(encoder.matches(PASSWORD, "no:delimiter:in:wrong:places")).isFalse(); |
||||
assertThat(encoder.matches(PASSWORD, "invalid_base64!:hash")).isFalse(); |
||||
} |
||||
|
||||
@Test |
||||
void multipleEncodingsShouldProduceDifferentHashesButAllMatch() { |
||||
BalloonHashingPassword4jPasswordEncoder encoder = new BalloonHashingPassword4jPasswordEncoder(); |
||||
|
||||
String encoded1 = encoder.encode(PASSWORD); |
||||
String encoded2 = encoder.encode(PASSWORD); |
||||
|
||||
assertThat(encoded1).isNotEqualTo(encoded2); // Different salts should produce
|
||||
// different results
|
||||
assertThat(encoder.matches(PASSWORD, encoded1)).isTrue(); |
||||
assertThat(encoder.matches(PASSWORD, encoded2)).isTrue(); |
||||
} |
||||
|
||||
@Test |
||||
void upgradeEncodingShouldReturnFalse() { |
||||
BalloonHashingPassword4jPasswordEncoder encoder = new BalloonHashingPassword4jPasswordEncoder(); |
||||
|
||||
String encoded = encoder.encode(PASSWORD); |
||||
boolean shouldUpgrade = encoder.upgradeEncoding(encoded); |
||||
|
||||
assertThat(shouldUpgrade).isFalse(); |
||||
} |
||||
|
||||
@Test |
||||
void encodeNullShouldReturnNull() { |
||||
BalloonHashingPassword4jPasswordEncoder encoder = new BalloonHashingPassword4jPasswordEncoder(); |
||||
|
||||
assertThat(encoder.encode(null)).isNull(); |
||||
} |
||||
|
||||
@Test |
||||
void matchesWithNullOrEmptyValuesShouldReturnFalse() { |
||||
BalloonHashingPassword4jPasswordEncoder encoder = new BalloonHashingPassword4jPasswordEncoder(); |
||||
String encoded = encoder.encode(PASSWORD); |
||||
|
||||
assertThat(encoder.matches(null, encoded)).isFalse(); |
||||
assertThat(encoder.matches("", encoded)).isFalse(); |
||||
assertThat(encoder.matches(PASSWORD, null)).isFalse(); |
||||
assertThat(encoder.matches(PASSWORD, "")).isFalse(); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,167 @@
@@ -0,0 +1,167 @@
|
||||
/* |
||||
* 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.PBKDF2Function; |
||||
import org.junit.jupiter.api.Test; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; |
||||
|
||||
/** |
||||
* Tests for {@link Pbkdf2Password4jPasswordEncoder}. |
||||
* |
||||
* @author Mehrdad Bozorgmehr |
||||
*/ |
||||
class Pbkdf2Password4jPasswordEncoderTests { |
||||
|
||||
private static final String PASSWORD = "password"; |
||||
|
||||
private static final String DIFFERENT_PASSWORD = "differentpassword"; |
||||
|
||||
@Test |
||||
void constructorWithNullFunctionShouldThrowException() { |
||||
assertThatIllegalArgumentException().isThrownBy(() -> new Pbkdf2Password4jPasswordEncoder(null)) |
||||
.withMessage("pbkdf2Function cannot be null"); |
||||
} |
||||
|
||||
@Test |
||||
void constructorWithInvalidSaltLengthShouldThrowException() { |
||||
PBKDF2Function function = AlgorithmFinder.getPBKDF2Instance(); |
||||
assertThatIllegalArgumentException().isThrownBy(() -> new Pbkdf2Password4jPasswordEncoder(function, 0)) |
||||
.withMessage("saltLength must be positive"); |
||||
assertThatIllegalArgumentException().isThrownBy(() -> new Pbkdf2Password4jPasswordEncoder(function, -1)) |
||||
.withMessage("saltLength must be positive"); |
||||
} |
||||
|
||||
@Test |
||||
void defaultConstructorShouldWork() { |
||||
Pbkdf2Password4jPasswordEncoder encoder = new Pbkdf2Password4jPasswordEncoder(); |
||||
|
||||
String encoded = encoder.encode(PASSWORD); |
||||
|
||||
assertThat(encoded).isNotNull().isNotEqualTo(PASSWORD).contains(":"); |
||||
assertThat(encoder.matches(PASSWORD, encoded)).isTrue(); |
||||
} |
||||
|
||||
@Test |
||||
void customFunctionConstructorShouldWork() { |
||||
PBKDF2Function customFunction = AlgorithmFinder.getPBKDF2Instance(); |
||||
Pbkdf2Password4jPasswordEncoder encoder = new Pbkdf2Password4jPasswordEncoder(customFunction); |
||||
|
||||
String encoded = encoder.encode(PASSWORD); |
||||
|
||||
assertThat(encoded).isNotNull().isNotEqualTo(PASSWORD).contains(":"); |
||||
assertThat(encoder.matches(PASSWORD, encoded)).isTrue(); |
||||
} |
||||
|
||||
@Test |
||||
void customSaltLengthConstructorShouldWork() { |
||||
PBKDF2Function function = AlgorithmFinder.getPBKDF2Instance(); |
||||
Pbkdf2Password4jPasswordEncoder encoder = new Pbkdf2Password4jPasswordEncoder(function, 16); |
||||
|
||||
String encoded = encoder.encode(PASSWORD); |
||||
|
||||
assertThat(encoded).isNotNull().isNotEqualTo(PASSWORD).contains(":"); |
||||
assertThat(encoder.matches(PASSWORD, encoded)).isTrue(); |
||||
} |
||||
|
||||
@Test |
||||
void encodeShouldIncludeSaltInOutput() { |
||||
Pbkdf2Password4jPasswordEncoder encoder = new Pbkdf2Password4jPasswordEncoder(); |
||||
|
||||
String encoded = encoder.encode(PASSWORD); |
||||
|
||||
assertThat(encoded).contains(":"); |
||||
String[] parts = encoded.split(":"); |
||||
assertThat(parts).hasSize(2); |
||||
assertThat(parts[0]).isNotEmpty(); // salt part
|
||||
assertThat(parts[1]).isNotEmpty(); // hash part
|
||||
} |
||||
|
||||
@Test |
||||
void matchesShouldReturnTrueForCorrectPassword() { |
||||
Pbkdf2Password4jPasswordEncoder encoder = new Pbkdf2Password4jPasswordEncoder(); |
||||
|
||||
String encoded = encoder.encode(PASSWORD); |
||||
boolean matches = encoder.matches(PASSWORD, encoded); |
||||
|
||||
assertThat(matches).isTrue(); |
||||
} |
||||
|
||||
@Test |
||||
void matchesShouldReturnFalseForIncorrectPassword() { |
||||
Pbkdf2Password4jPasswordEncoder encoder = new Pbkdf2Password4jPasswordEncoder(); |
||||
|
||||
String encoded = encoder.encode(PASSWORD); |
||||
boolean matches = encoder.matches(DIFFERENT_PASSWORD, encoded); |
||||
|
||||
assertThat(matches).isFalse(); |
||||
} |
||||
|
||||
@Test |
||||
void matchesShouldReturnFalseForMalformedEncodedPassword() { |
||||
Pbkdf2Password4jPasswordEncoder encoder = new Pbkdf2Password4jPasswordEncoder(); |
||||
|
||||
assertThat(encoder.matches(PASSWORD, "malformed")).isFalse(); |
||||
assertThat(encoder.matches(PASSWORD, "no:delimiter:in:wrong:places")).isFalse(); |
||||
assertThat(encoder.matches(PASSWORD, "invalid_base64!:hash")).isFalse(); |
||||
} |
||||
|
||||
@Test |
||||
void multipleEncodingsShouldProduceDifferentHashesButAllMatch() { |
||||
Pbkdf2Password4jPasswordEncoder encoder = new Pbkdf2Password4jPasswordEncoder(); |
||||
|
||||
String encoded1 = encoder.encode(PASSWORD); |
||||
String encoded2 = encoder.encode(PASSWORD); |
||||
|
||||
assertThat(encoded1).isNotEqualTo(encoded2); // Different salts should produce
|
||||
// different results
|
||||
assertThat(encoder.matches(PASSWORD, encoded1)).isTrue(); |
||||
assertThat(encoder.matches(PASSWORD, encoded2)).isTrue(); |
||||
} |
||||
|
||||
@Test |
||||
void upgradeEncodingShouldReturnFalse() { |
||||
Pbkdf2Password4jPasswordEncoder encoder = new Pbkdf2Password4jPasswordEncoder(); |
||||
|
||||
String encoded = encoder.encode(PASSWORD); |
||||
boolean shouldUpgrade = encoder.upgradeEncoding(encoded); |
||||
|
||||
assertThat(shouldUpgrade).isFalse(); |
||||
} |
||||
|
||||
@Test |
||||
void encodeNullShouldReturnNull() { |
||||
Pbkdf2Password4jPasswordEncoder encoder = new Pbkdf2Password4jPasswordEncoder(); |
||||
|
||||
assertThat(encoder.encode(null)).isNull(); |
||||
} |
||||
|
||||
@Test |
||||
void matchesWithNullOrEmptyValuesShouldReturnFalse() { |
||||
Pbkdf2Password4jPasswordEncoder encoder = new Pbkdf2Password4jPasswordEncoder(); |
||||
String encoded = encoder.encode(PASSWORD); |
||||
|
||||
assertThat(encoder.matches(null, encoded)).isFalse(); |
||||
assertThat(encoder.matches("", encoded)).isFalse(); |
||||
assertThat(encoder.matches(PASSWORD, null)).isFalse(); |
||||
assertThat(encoder.matches(PASSWORD, "")).isFalse(); |
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue