mirror of
https://github.com/spring-projects/spring-security.git
synced 2025-10-23 18:59:46 +00:00
Add Argon2 and BCrypt and Scrypt password encoders using Password4j library
Closes gh-17706 Signed-off-by: Mehrdad <mehrdad.bozorgmehr@gmail.com> Signed-off-by: M.Bozorgmehr <mehrdad.bozorgmehr@gmail.com>
This commit is contained in:
parent
9f5d27e8d0
commit
8c2ad4e4d1
@ -0,0 +1,74 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of {@link org.springframework.security.crypto.password.PasswordEncoder}
|
||||||
|
* that uses the Password4j library with Argon2 hashing algorithm.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Argon2 is the winner of the Password Hashing Competition (2015) and is recommended for
|
||||||
|
* new applications. It provides excellent resistance against GPU-based attacks and
|
||||||
|
* includes built-in salt generation. This implementation leverages Password4j's Argon2
|
||||||
|
* support which properly includes the salt in the output hash.
|
||||||
|
* </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 Argon2 settings (recommended)
|
||||||
|
* PasswordEncoder encoder = new Argon2Password4jPasswordEncoder();
|
||||||
|
*
|
||||||
|
* // Using custom Argon2 configuration
|
||||||
|
* PasswordEncoder customEncoder = new Argon2Password4jPasswordEncoder(
|
||||||
|
* Argon2Function.getInstance(65536, 3, 4, 32, Argon2.ID));
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* @author Mehrdad Bozorgmehr
|
||||||
|
* @since 7.0
|
||||||
|
* @see Argon2Function
|
||||||
|
* @see AlgorithmFinder#getArgon2Instance()
|
||||||
|
*/
|
||||||
|
public class Argon2Password4jPasswordEncoder extends Password4jPasswordEncoder {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs an Argon2 password encoder using the default Argon2 configuration from
|
||||||
|
* Password4j's AlgorithmFinder.
|
||||||
|
*/
|
||||||
|
public Argon2Password4jPasswordEncoder() {
|
||||||
|
super(AlgorithmFinder.getArgon2Instance());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs an Argon2 password encoder with a custom Argon2 function.
|
||||||
|
* @param argon2Function the Argon2 function to use for encoding passwords, must not
|
||||||
|
* be null
|
||||||
|
* @throws IllegalArgumentException if argon2Function is null
|
||||||
|
*/
|
||||||
|
public Argon2Password4jPasswordEncoder(Argon2Function argon2Function) {
|
||||||
|
super(argon2Function);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,72 @@
|
|||||||
|
/*
|
||||||
|
* 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.BcryptFunction;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of {@link org.springframework.security.crypto.password.PasswordEncoder}
|
||||||
|
* that uses the Password4j library with BCrypt hashing algorithm.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* BCrypt is a well-established password hashing algorithm that includes built-in salt
|
||||||
|
* generation and is resistant to rainbow table attacks. This implementation leverages
|
||||||
|
* Password4j's BCrypt support which properly includes the salt in the output hash.
|
||||||
|
* </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 BCrypt settings (recommended)
|
||||||
|
* PasswordEncoder encoder = new BcryptPassword4jPasswordEncoder();
|
||||||
|
*
|
||||||
|
* // Using custom round count
|
||||||
|
* PasswordEncoder customEncoder = new BcryptPassword4jPasswordEncoder(BcryptFunction.getInstance(12));
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* @author Mehrdad Bozorgmehr
|
||||||
|
* @since 7.0
|
||||||
|
* @see BcryptFunction
|
||||||
|
* @see AlgorithmFinder#getBcryptInstance()
|
||||||
|
*/
|
||||||
|
public class BcryptPassword4jPasswordEncoder extends Password4jPasswordEncoder {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a BCrypt password encoder using the default BCrypt configuration from
|
||||||
|
* Password4j's AlgorithmFinder.
|
||||||
|
*/
|
||||||
|
public BcryptPassword4jPasswordEncoder() {
|
||||||
|
super(AlgorithmFinder.getBcryptInstance());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a BCrypt password encoder with a custom BCrypt function.
|
||||||
|
* @param bcryptFunction the BCrypt function to use for encoding passwords, must not
|
||||||
|
* be null
|
||||||
|
* @throws IllegalArgumentException if bcryptFunction is null
|
||||||
|
*/
|
||||||
|
public BcryptPassword4jPasswordEncoder(BcryptFunction bcryptFunction) {
|
||||||
|
super(bcryptFunction);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -16,117 +16,57 @@
|
|||||||
|
|
||||||
package org.springframework.security.crypto.password4j;
|
package org.springframework.security.crypto.password4j;
|
||||||
|
|
||||||
import com.password4j.AlgorithmFinder;
|
|
||||||
import com.password4j.Hash;
|
import com.password4j.Hash;
|
||||||
import com.password4j.HashingFunction;
|
import com.password4j.HashingFunction;
|
||||||
import com.password4j.Password;
|
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.security.crypto.password.AbstractValidatingPasswordEncoder;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implementation of {@link org.springframework.security.crypto.password.PasswordEncoder}
|
* Abstract base class for Password4j-based password encoders. This class provides the
|
||||||
* that uses the Password4j library. This encoder supports multiple password hashing
|
* common functionality for password encoding and verification using the Password4j
|
||||||
* algorithms including BCrypt, SCrypt, Argon2, and PBKDF2.
|
* library.
|
||||||
*
|
*
|
||||||
* <p>
|
* <p>
|
||||||
* The encoder uses the provided {@link HashingFunction} for both encoding and
|
* This class is package-private and should not be used directly. Instead, use the
|
||||||
* verification. Password4j can automatically detect the algorithm used in existing hashes
|
* specific public subclasses that support verified hashing algorithms such as BCrypt,
|
||||||
* during verification.
|
* Argon2, and SCrypt implementations.
|
||||||
* </p>
|
* </p>
|
||||||
*
|
*
|
||||||
* <p>
|
* <p>
|
||||||
* This implementation is thread-safe and can be shared across multiple threads.
|
* This implementation is thread-safe and can be shared across multiple threads.
|
||||||
* </p>
|
* </p>
|
||||||
*
|
*
|
||||||
* <p>
|
|
||||||
* <strong>Usage Examples:</strong>
|
|
||||||
* </p>
|
|
||||||
* <pre>{@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));
|
|
||||||
* }</pre>
|
|
||||||
*
|
|
||||||
* @author Mehrdad Bozorgmehr
|
* @author Mehrdad Bozorgmehr
|
||||||
* @since 7.0
|
* @since 7.0
|
||||||
* @see AlgorithmFinder
|
|
||||||
*/
|
*/
|
||||||
public class Password4jPasswordEncoder extends AbstractValidatingPasswordEncoder {
|
abstract class Password4jPasswordEncoder extends AbstractValidatingPasswordEncoder {
|
||||||
|
|
||||||
private final Log logger = LogFactory.getLog(getClass());
|
|
||||||
|
|
||||||
private final HashingFunction hashingFunction;
|
private final HashingFunction hashingFunction;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a Password4j password encoder with the specified hashing function.
|
* Constructs a Password4j password encoder with the specified hashing function. This
|
||||||
*
|
* constructor is package-private and intended for use by subclasses only.
|
||||||
* <p>
|
|
||||||
* It is recommended to use password4j's {@link AlgorithmFinder} to obtain default
|
|
||||||
* instances with secure configurations:
|
|
||||||
* </p>
|
|
||||||
* <ul>
|
|
||||||
* <li>{@code AlgorithmFinder.getBcryptInstance()} - BCrypt with default settings</li>
|
|
||||||
* <li>{@code AlgorithmFinder.getArgon2Instance()} - Argon2 with default settings</li>
|
|
||||||
* <li>{@code AlgorithmFinder.getScryptInstance()} - SCrypt with default settings</li>
|
|
||||||
* <li>{@code AlgorithmFinder.getPBKDF2Instance()} - PBKDF2 with default settings</li>
|
|
||||||
* </ul>
|
|
||||||
*
|
|
||||||
* <p>
|
|
||||||
* For custom configurations, you can create specific function instances:
|
|
||||||
* </p>
|
|
||||||
* <ul>
|
|
||||||
* <li>{@code BcryptFunction.getInstance(12)} - BCrypt with 12 rounds</li>
|
|
||||||
* <li>{@code Argon2Function.getInstance(65536, 3, 4, 32, Argon2.ID)} - Custom
|
|
||||||
* Argon2</li>
|
|
||||||
* <li>{@code ScryptFunction.getInstance(16384, 8, 1, 32)} - Custom SCrypt</li>
|
|
||||||
* <li>{@code CompressedPBKDF2Function.getInstance("SHA256", 310000, 32)} - Custom
|
|
||||||
* PBKDF2</li>
|
|
||||||
* </ul>
|
|
||||||
* @param hashingFunction the hashing function to use for encoding passwords, must not
|
* @param hashingFunction the hashing function to use for encoding passwords, must not
|
||||||
* be null
|
* be null
|
||||||
* @throws IllegalArgumentException if hashingFunction is null
|
* @throws IllegalArgumentException if hashingFunction is null
|
||||||
*/
|
*/
|
||||||
public Password4jPasswordEncoder(HashingFunction hashingFunction) {
|
Password4jPasswordEncoder(HashingFunction hashingFunction) {
|
||||||
Assert.notNull(hashingFunction, "hashingFunction cannot be null");
|
Assert.notNull(hashingFunction, "hashingFunction cannot be null");
|
||||||
this.hashingFunction = hashingFunction;
|
this.hashingFunction = hashingFunction;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected String encodeNonNullPassword(String rawPassword) {
|
protected String encodeNonNullPassword(String rawPassword) {
|
||||||
try {
|
|
||||||
Hash hash = Password.hash(rawPassword).with(this.hashingFunction);
|
Hash hash = Password.hash(rawPassword).with(this.hashingFunction);
|
||||||
return hash.getResult();
|
return hash.getResult();
|
||||||
}
|
}
|
||||||
catch (Exception ex) {
|
|
||||||
throw new IllegalStateException("Failed to encode password using Password4j", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean matchesNonNull(String rawPassword, String encodedPassword) {
|
protected boolean matchesNonNull(String rawPassword, String encodedPassword) {
|
||||||
try {
|
|
||||||
// Use the specific hashing function for verification
|
|
||||||
return Password.check(rawPassword, encodedPassword).with(this.hashingFunction);
|
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
|
@Override
|
||||||
protected boolean upgradeEncodingNonNull(String encodedPassword) {
|
protected boolean upgradeEncodingNonNull(String encodedPassword) {
|
||||||
|
@ -0,0 +1,74 @@
|
|||||||
|
/*
|
||||||
|
* 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.ScryptFunction;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of {@link org.springframework.security.crypto.password.PasswordEncoder}
|
||||||
|
* that uses the Password4j library with SCrypt hashing algorithm.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* SCrypt is a memory-hard password hashing algorithm designed to be resistant to hardware
|
||||||
|
* brute-force attacks. It includes built-in salt generation and is particularly effective
|
||||||
|
* against ASIC and GPU-based attacks. This implementation leverages Password4j's SCrypt
|
||||||
|
* support which properly includes the salt in the output hash.
|
||||||
|
* </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 SCrypt settings (recommended)
|
||||||
|
* PasswordEncoder encoder = new ScryptPassword4jPasswordEncoder();
|
||||||
|
*
|
||||||
|
* // Using custom SCrypt configuration
|
||||||
|
* PasswordEncoder customEncoder = new ScryptPassword4jPasswordEncoder(
|
||||||
|
* ScryptFunction.getInstance(32768, 8, 1, 32));
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* @author Mehrdad Bozorgmehr
|
||||||
|
* @since 7.0
|
||||||
|
* @see ScryptFunction
|
||||||
|
* @see AlgorithmFinder#getScryptInstance()
|
||||||
|
*/
|
||||||
|
public class ScryptPassword4jPasswordEncoder extends Password4jPasswordEncoder {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs an SCrypt password encoder using the default SCrypt configuration from
|
||||||
|
* Password4j's AlgorithmFinder.
|
||||||
|
*/
|
||||||
|
public ScryptPassword4jPasswordEncoder() {
|
||||||
|
super(AlgorithmFinder.getScryptInstance());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs an SCrypt password encoder with a custom SCrypt function.
|
||||||
|
* @param scryptFunction the SCrypt function to use for encoding passwords, must not
|
||||||
|
* be null
|
||||||
|
* @throws IllegalArgumentException if scryptFunction is null
|
||||||
|
*/
|
||||||
|
public ScryptPassword4jPasswordEncoder(ScryptFunction scryptFunction) {
|
||||||
|
super(scryptFunction);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,245 @@
|
|||||||
|
/*
|
||||||
|
* 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.types.Argon2;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.EnumSource;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link Argon2Password4jPasswordEncoder}.
|
||||||
|
*
|
||||||
|
* @author Mehrdad Bozorgmehr
|
||||||
|
*/
|
||||||
|
class Argon2Password4jPasswordEncoderTests {
|
||||||
|
|
||||||
|
private static final String PASSWORD = "password";
|
||||||
|
|
||||||
|
private static final String LONG_PASSWORD = "a".repeat(1000);
|
||||||
|
|
||||||
|
private static final String SPECIAL_CHARS_PASSWORD = "p@ssw0rd!#$%^&*()_+-=[]{}|;':\",./<>?";
|
||||||
|
|
||||||
|
private static final String UNICODE_PASSWORD = "пароль密码パスワード🔐";
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void defaultConstructorShouldCreateWorkingEncoder() {
|
||||||
|
Argon2Password4jPasswordEncoder encoder = new Argon2Password4jPasswordEncoder();
|
||||||
|
|
||||||
|
String encoded = encoder.encode(PASSWORD);
|
||||||
|
|
||||||
|
assertThat(encoded).isNotNull();
|
||||||
|
assertThat(encoded).startsWith("$argon2"); // Argon2 hash format
|
||||||
|
assertThat(encoder.matches(PASSWORD, encoded)).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void constructorWithNullArgon2FunctionShouldThrowException() {
|
||||||
|
assertThatIllegalArgumentException().isThrownBy(() -> new Argon2Password4jPasswordEncoder(null))
|
||||||
|
.withMessage("hashingFunction cannot be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void constructorWithCustomArgon2FunctionShouldWork() {
|
||||||
|
Argon2Function customFunction = Argon2Function.getInstance(4096, 3, 1, 32, Argon2.ID);
|
||||||
|
Argon2Password4jPasswordEncoder encoder = new Argon2Password4jPasswordEncoder(customFunction);
|
||||||
|
|
||||||
|
String encoded = encoder.encode(PASSWORD);
|
||||||
|
|
||||||
|
assertThat(encoded).isNotNull();
|
||||||
|
assertThat(encoded).startsWith("$argon2id");
|
||||||
|
assertThat(encoder.matches(PASSWORD, encoded)).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@EnumSource(Argon2.class)
|
||||||
|
void encodingShouldWorkWithDifferentArgon2Types(Argon2 type) {
|
||||||
|
Argon2Function function = Argon2Function.getInstance(4096, 3, 1, 32, type);
|
||||||
|
Argon2Password4jPasswordEncoder encoder = new Argon2Password4jPasswordEncoder(function);
|
||||||
|
|
||||||
|
String encoded = encoder.encode(PASSWORD);
|
||||||
|
|
||||||
|
assertThat(encoded).isNotNull();
|
||||||
|
assertThat(encoded).startsWith("$argon2" + type.name().toLowerCase());
|
||||||
|
assertThat(encoder.matches(PASSWORD, encoded)).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void encodingShouldGenerateDifferentHashesForSamePassword() {
|
||||||
|
Argon2Password4jPasswordEncoder encoder = new Argon2Password4jPasswordEncoder();
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldHandleLongPasswords() {
|
||||||
|
Argon2Password4jPasswordEncoder encoder = new Argon2Password4jPasswordEncoder();
|
||||||
|
|
||||||
|
String encoded = encoder.encode(LONG_PASSWORD);
|
||||||
|
|
||||||
|
assertThat(encoder.matches(LONG_PASSWORD, encoded)).isTrue();
|
||||||
|
assertThat(encoder.matches("wrong", encoded)).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldHandleSpecialCharacters() {
|
||||||
|
Argon2Password4jPasswordEncoder encoder = new Argon2Password4jPasswordEncoder();
|
||||||
|
|
||||||
|
String encoded = encoder.encode(SPECIAL_CHARS_PASSWORD);
|
||||||
|
|
||||||
|
assertThat(encoder.matches(SPECIAL_CHARS_PASSWORD, encoded)).isTrue();
|
||||||
|
assertThat(encoder.matches("wrong", encoded)).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldHandleUnicodeCharacters() {
|
||||||
|
Argon2Password4jPasswordEncoder encoder = new Argon2Password4jPasswordEncoder();
|
||||||
|
|
||||||
|
String encoded = encoder.encode(UNICODE_PASSWORD);
|
||||||
|
|
||||||
|
assertThat(encoder.matches(UNICODE_PASSWORD, encoded)).isTrue();
|
||||||
|
assertThat(encoder.matches("wrong", encoded)).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRejectIncorrectPasswords() {
|
||||||
|
Argon2Password4jPasswordEncoder encoder = new Argon2Password4jPasswordEncoder();
|
||||||
|
String encoded = encoder.encode(PASSWORD);
|
||||||
|
|
||||||
|
assertThat(encoder.matches("wrongpassword", encoded)).isFalse();
|
||||||
|
assertThat(encoder.matches("PASSWORD", encoded)).isFalse(); // Case sensitive
|
||||||
|
assertThat(encoder.matches("password ", encoded)).isFalse(); // Trailing space
|
||||||
|
assertThat(encoder.matches(" password", encoded)).isFalse(); // Leading space
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void matchesShouldReturnFalseForNullOrEmptyInputs() {
|
||||||
|
Argon2Password4jPasswordEncoder encoder = new Argon2Password4jPasswordEncoder();
|
||||||
|
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();
|
||||||
|
assertThat(encoder.matches(null, null)).isFalse();
|
||||||
|
assertThat(encoder.matches("", "")).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void encodeNullShouldReturnNull() {
|
||||||
|
Argon2Password4jPasswordEncoder encoder = new Argon2Password4jPasswordEncoder();
|
||||||
|
|
||||||
|
assertThat(encoder.encode(null)).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void upgradeEncodingShouldReturnFalse() {
|
||||||
|
Argon2Password4jPasswordEncoder encoder = new Argon2Password4jPasswordEncoder();
|
||||||
|
String encoded = encoder.encode(PASSWORD);
|
||||||
|
|
||||||
|
assertThat(encoder.upgradeEncoding(encoded)).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldWorkWithAlgorithmFinderDefaults() {
|
||||||
|
Argon2Password4jPasswordEncoder encoder = new Argon2Password4jPasswordEncoder(
|
||||||
|
AlgorithmFinder.getArgon2Instance());
|
||||||
|
|
||||||
|
String encoded = encoder.encode(PASSWORD);
|
||||||
|
|
||||||
|
assertThat(encoded).isNotNull();
|
||||||
|
assertThat(encoder.matches(PASSWORD, encoded)).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRejectMalformedHashes() {
|
||||||
|
Argon2Password4jPasswordEncoder encoder = new Argon2Password4jPasswordEncoder();
|
||||||
|
|
||||||
|
// For Argon2, Password4j may throw BadParametersException on malformed hashes.
|
||||||
|
// We treat either an exception or a false return as a successful rejection.
|
||||||
|
assertMalformedRejected(encoder, PASSWORD, "invalid_hash");
|
||||||
|
assertMalformedRejected(encoder, PASSWORD, "$argon2id$invalid");
|
||||||
|
assertMalformedRejected(encoder, PASSWORD, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertMalformedRejected(Argon2Password4jPasswordEncoder encoder, String raw, String malformed) {
|
||||||
|
boolean rejected = false;
|
||||||
|
try {
|
||||||
|
rejected = !encoder.matches(raw, malformed);
|
||||||
|
}
|
||||||
|
catch (RuntimeException ex) {
|
||||||
|
// Accept exception as valid rejection path for malformed input
|
||||||
|
rejected = true;
|
||||||
|
}
|
||||||
|
assertThat(rejected).as("Malformed hash should not validate: " + malformed).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldHandleEmptyStringPassword() {
|
||||||
|
Argon2Password4jPasswordEncoder encoder = new Argon2Password4jPasswordEncoder();
|
||||||
|
|
||||||
|
String encoded = encoder.encode("");
|
||||||
|
|
||||||
|
assertThat(encoded).isNotNull();
|
||||||
|
boolean emptyStringMatches;
|
||||||
|
try {
|
||||||
|
emptyStringMatches = encoder.matches("", encoded);
|
||||||
|
}
|
||||||
|
catch (RuntimeException ex) {
|
||||||
|
emptyStringMatches = false; // treat exception as non-match but still
|
||||||
|
// acceptable behavior
|
||||||
|
}
|
||||||
|
|
||||||
|
if (emptyStringMatches) {
|
||||||
|
assertThat(encoder.matches("", encoded)).isTrue();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
assertThat(encoded).isNotEmpty();
|
||||||
|
}
|
||||||
|
assertThat(encoder.matches("notEmpty", encoded)).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldHandleCustomMemoryAndIterationParameters() {
|
||||||
|
// Test with different memory and iteration parameters
|
||||||
|
Argon2Function lowMemory = Argon2Function.getInstance(1024, 2, 1, 16, Argon2.ID);
|
||||||
|
Argon2Function highMemory = Argon2Function.getInstance(65536, 4, 2, 64, Argon2.ID);
|
||||||
|
|
||||||
|
Argon2Password4jPasswordEncoder lowEncoder = new Argon2Password4jPasswordEncoder(lowMemory);
|
||||||
|
Argon2Password4jPasswordEncoder highEncoder = new Argon2Password4jPasswordEncoder(highMemory);
|
||||||
|
|
||||||
|
String lowEncoded = lowEncoder.encode(PASSWORD);
|
||||||
|
String highEncoded = highEncoder.encode(PASSWORD);
|
||||||
|
|
||||||
|
assertThat(lowEncoder.matches(PASSWORD, lowEncoded)).isTrue();
|
||||||
|
assertThat(highEncoder.matches(PASSWORD, highEncoded)).isTrue();
|
||||||
|
|
||||||
|
// Each encoder should work with hashes generated by the same parameters
|
||||||
|
assertThat(lowEncoded).isNotEqualTo(highEncoded);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,217 @@
|
|||||||
|
/*
|
||||||
|
* 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.BcryptFunction;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
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 BcryptPassword4jPasswordEncoder}.
|
||||||
|
*
|
||||||
|
* @author Mehrdad Bozorgmehr
|
||||||
|
*/
|
||||||
|
class BcryptPassword4jPasswordEncoderTests {
|
||||||
|
|
||||||
|
private static final String PASSWORD = "password";
|
||||||
|
|
||||||
|
private static final String LONG_PASSWORD = "a".repeat(72); // BCrypt max length
|
||||||
|
|
||||||
|
private static final String VERY_LONG_PASSWORD = "a".repeat(100); // Beyond BCrypt max
|
||||||
|
// length
|
||||||
|
|
||||||
|
private static final String SPECIAL_CHARS_PASSWORD = "p@ssw0rd!#$%^&*()_+-=[]{}|;':\",./<>?";
|
||||||
|
|
||||||
|
private static final String UNICODE_PASSWORD = "пароль密码パスワード🔐";
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void defaultConstructorShouldCreateWorkingEncoder() {
|
||||||
|
BcryptPassword4jPasswordEncoder encoder = new BcryptPassword4jPasswordEncoder();
|
||||||
|
|
||||||
|
String encoded = encoder.encode(PASSWORD);
|
||||||
|
|
||||||
|
assertThat(encoded).isNotNull().matches("^\\$2[aby]?\\$.*");
|
||||||
|
assertThat(encoder.matches(PASSWORD, encoded)).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void constructorWithNullBcryptFunctionShouldThrowException() {
|
||||||
|
assertThatIllegalArgumentException().isThrownBy(() -> new BcryptPassword4jPasswordEncoder(null))
|
||||||
|
.withMessage("hashingFunction cannot be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void constructorWithCustomBcryptFunctionShouldWork() {
|
||||||
|
BcryptFunction customFunction = BcryptFunction.getInstance(6);
|
||||||
|
BcryptPassword4jPasswordEncoder encoder = new BcryptPassword4jPasswordEncoder(customFunction);
|
||||||
|
|
||||||
|
String encoded = encoder.encode(PASSWORD);
|
||||||
|
|
||||||
|
assertThat(encoded).isNotNull().contains("$06$"); // 6 rounds
|
||||||
|
assertThat(encoder.matches(PASSWORD, encoded)).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@ValueSource(ints = { 4, 6, 8, 10, 12 })
|
||||||
|
void encodingShouldWorkWithDifferentRounds(int rounds) {
|
||||||
|
BcryptFunction function = BcryptFunction.getInstance(rounds);
|
||||||
|
BcryptPassword4jPasswordEncoder encoder = new BcryptPassword4jPasswordEncoder(function);
|
||||||
|
|
||||||
|
String encoded = encoder.encode(PASSWORD);
|
||||||
|
|
||||||
|
assertThat(encoded).isNotNull().contains(String.format("$%02d$", rounds));
|
||||||
|
assertThat(encoder.matches(PASSWORD, encoded)).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void encodingShouldGenerateDifferentHashesForSamePassword() {
|
||||||
|
BcryptPassword4jPasswordEncoder encoder = new BcryptPassword4jPasswordEncoder();
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldHandleLongPasswords() {
|
||||||
|
BcryptPassword4jPasswordEncoder encoder = new BcryptPassword4jPasswordEncoder();
|
||||||
|
|
||||||
|
String encodedLong = encoder.encode(LONG_PASSWORD);
|
||||||
|
String encodedVeryLong = encoder.encode(VERY_LONG_PASSWORD);
|
||||||
|
|
||||||
|
assertThat(encoder.matches(LONG_PASSWORD, encodedLong)).isTrue();
|
||||||
|
assertThat(encoder.matches(VERY_LONG_PASSWORD, encodedVeryLong)).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldHandleSpecialCharacters() {
|
||||||
|
BcryptPassword4jPasswordEncoder encoder = new BcryptPassword4jPasswordEncoder();
|
||||||
|
|
||||||
|
String encoded = encoder.encode(SPECIAL_CHARS_PASSWORD);
|
||||||
|
|
||||||
|
assertThat(encoder.matches(SPECIAL_CHARS_PASSWORD, encoded)).isTrue();
|
||||||
|
assertThat(encoder.matches("wrong", encoded)).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldHandleUnicodeCharacters() {
|
||||||
|
BcryptPassword4jPasswordEncoder encoder = new BcryptPassword4jPasswordEncoder();
|
||||||
|
|
||||||
|
String encoded = encoder.encode(UNICODE_PASSWORD);
|
||||||
|
|
||||||
|
assertThat(encoder.matches(UNICODE_PASSWORD, encoded)).isTrue();
|
||||||
|
assertThat(encoder.matches("wrong", encoded)).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRejectIncorrectPasswords() {
|
||||||
|
BcryptPassword4jPasswordEncoder encoder = new BcryptPassword4jPasswordEncoder();
|
||||||
|
String encoded = encoder.encode(PASSWORD);
|
||||||
|
|
||||||
|
assertThat(encoder.matches("wrongpassword", encoded)).isFalse();
|
||||||
|
assertThat(encoder.matches("PASSWORD", encoded)).isFalse(); // Case sensitive
|
||||||
|
assertThat(encoder.matches("password ", encoded)).isFalse(); // Trailing space
|
||||||
|
assertThat(encoder.matches(" password", encoded)).isFalse(); // Leading space
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void matchesShouldReturnFalseForNullOrEmptyInputs() {
|
||||||
|
BcryptPassword4jPasswordEncoder encoder = new BcryptPassword4jPasswordEncoder();
|
||||||
|
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();
|
||||||
|
assertThat(encoder.matches(null, null)).isFalse();
|
||||||
|
assertThat(encoder.matches("", "")).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void encodeNullShouldReturnNull() {
|
||||||
|
BcryptPassword4jPasswordEncoder encoder = new BcryptPassword4jPasswordEncoder();
|
||||||
|
|
||||||
|
assertThat(encoder.encode(null)).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void upgradeEncodingShouldReturnFalse() {
|
||||||
|
BcryptPassword4jPasswordEncoder encoder = new BcryptPassword4jPasswordEncoder();
|
||||||
|
String encoded = encoder.encode(PASSWORD);
|
||||||
|
|
||||||
|
assertThat(encoder.upgradeEncoding(encoded)).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldWorkWithAlgorithmFinderDefaults() {
|
||||||
|
BcryptPassword4jPasswordEncoder encoder = new BcryptPassword4jPasswordEncoder(
|
||||||
|
AlgorithmFinder.getBcryptInstance());
|
||||||
|
|
||||||
|
String encoded = encoder.encode(PASSWORD);
|
||||||
|
|
||||||
|
assertThat(encoded).isNotNull();
|
||||||
|
assertThat(encoder.matches(PASSWORD, encoded)).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRejectMalformedHashes() {
|
||||||
|
BcryptPassword4jPasswordEncoder encoder = new BcryptPassword4jPasswordEncoder();
|
||||||
|
assertMalformedRejected(encoder, PASSWORD, "invalid_hash");
|
||||||
|
assertMalformedRejected(encoder, PASSWORD, "$2a$10$invalid");
|
||||||
|
assertMalformedRejected(encoder, PASSWORD, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertMalformedRejected(BcryptPassword4jPasswordEncoder encoder, String raw, String malformed) {
|
||||||
|
boolean rejected;
|
||||||
|
try {
|
||||||
|
rejected = !encoder.matches(raw, malformed);
|
||||||
|
}
|
||||||
|
catch (RuntimeException ex) {
|
||||||
|
rejected = true; // exception is acceptable rejection
|
||||||
|
}
|
||||||
|
assertThat(rejected).as("Malformed hash should not validate: " + malformed).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldHandleEmptyStringPassword() {
|
||||||
|
BcryptPassword4jPasswordEncoder encoder = new BcryptPassword4jPasswordEncoder();
|
||||||
|
String encoded = encoder.encode("");
|
||||||
|
assertThat(encoded).isNotNull();
|
||||||
|
boolean emptyMatches;
|
||||||
|
try {
|
||||||
|
emptyMatches = encoder.matches("", encoded);
|
||||||
|
}
|
||||||
|
catch (RuntimeException ex) {
|
||||||
|
emptyMatches = false; // treat as non-match if library rejects empty raw
|
||||||
|
}
|
||||||
|
// Either behavior acceptable; if it matches, verify; if not, still ensure other
|
||||||
|
// mismatches remain false.
|
||||||
|
if (emptyMatches) {
|
||||||
|
assertThat(encoder.matches("", encoded)).isTrue();
|
||||||
|
}
|
||||||
|
assertThat(encoder.matches("notEmpty", encoded)).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -16,16 +16,14 @@
|
|||||||
|
|
||||||
package org.springframework.security.crypto.password4j;
|
package org.springframework.security.crypto.password4j;
|
||||||
|
|
||||||
import com.password4j.AlgorithmFinder;
|
|
||||||
import com.password4j.BcryptFunction;
|
import com.password4j.BcryptFunction;
|
||||||
import com.password4j.HashingFunction;
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests for {@link Password4jPasswordEncoder}.
|
* Base functionality tests for {@link Password4jPasswordEncoder} implementations. These
|
||||||
|
* tests verify the common behavior across all concrete password encoder subclasses.
|
||||||
*
|
*
|
||||||
* @author Mehrdad Bozorgmehr
|
* @author Mehrdad Bozorgmehr
|
||||||
*/
|
*/
|
||||||
@ -35,27 +33,10 @@ class Password4jPasswordEncoderTests {
|
|||||||
|
|
||||||
private static final String WRONG_PASSWORD = "wrongpassword";
|
private static final String WRONG_PASSWORD = "wrongpassword";
|
||||||
|
|
||||||
// Constructor Tests
|
// Test abstract class behavior through concrete implementation
|
||||||
@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
|
@Test
|
||||||
void encodeShouldReturnNonNullHashedPassword() {
|
void encodeShouldReturnNonNullHashedPassword() {
|
||||||
HashingFunction hashingFunction = BcryptFunction.getInstance(4); // Use low cost
|
BcryptPassword4jPasswordEncoder encoder = new BcryptPassword4jPasswordEncoder(BcryptFunction.getInstance(4));
|
||||||
// for faster
|
|
||||||
// tests
|
|
||||||
Password4jPasswordEncoder encoder = new Password4jPasswordEncoder(hashingFunction);
|
|
||||||
|
|
||||||
String result = encoder.encode(PASSWORD);
|
String result = encoder.encode(PASSWORD);
|
||||||
|
|
||||||
@ -64,10 +45,7 @@ class Password4jPasswordEncoderTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void matchesShouldReturnTrueForValidPassword() {
|
void matchesShouldReturnTrueForValidPassword() {
|
||||||
HashingFunction hashingFunction = BcryptFunction.getInstance(4); // Use low cost
|
BcryptPassword4jPasswordEncoder encoder = new BcryptPassword4jPasswordEncoder(BcryptFunction.getInstance(4));
|
||||||
// for faster
|
|
||||||
// tests
|
|
||||||
Password4jPasswordEncoder encoder = new Password4jPasswordEncoder(hashingFunction);
|
|
||||||
|
|
||||||
String encoded = encoder.encode(PASSWORD);
|
String encoded = encoder.encode(PASSWORD);
|
||||||
boolean result = encoder.matches(PASSWORD, encoded);
|
boolean result = encoder.matches(PASSWORD, encoded);
|
||||||
@ -77,10 +55,7 @@ class Password4jPasswordEncoderTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void matchesShouldReturnFalseForInvalidPassword() {
|
void matchesShouldReturnFalseForInvalidPassword() {
|
||||||
HashingFunction hashingFunction = BcryptFunction.getInstance(4); // Use low cost
|
BcryptPassword4jPasswordEncoder encoder = new BcryptPassword4jPasswordEncoder(BcryptFunction.getInstance(4));
|
||||||
// for faster
|
|
||||||
// tests
|
|
||||||
Password4jPasswordEncoder encoder = new Password4jPasswordEncoder(hashingFunction);
|
|
||||||
|
|
||||||
String encoded = encoder.encode(PASSWORD);
|
String encoded = encoder.encode(PASSWORD);
|
||||||
boolean result = encoder.matches(WRONG_PASSWORD, encoded);
|
boolean result = encoder.matches(WRONG_PASSWORD, encoded);
|
||||||
@ -89,20 +64,27 @@ class Password4jPasswordEncoderTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void matchesShouldReturnFalseForMalformedHash() {
|
void encodeNullPasswordShouldReturnNull() {
|
||||||
HashingFunction hashingFunction = BcryptFunction.getInstance(4);
|
BcryptPassword4jPasswordEncoder encoder = new BcryptPassword4jPasswordEncoder(BcryptFunction.getInstance(4));
|
||||||
Password4jPasswordEncoder encoder = new Password4jPasswordEncoder(hashingFunction);
|
|
||||||
|
|
||||||
// Test with malformed hash that should cause Password4j to throw an exception
|
assertThat(encoder.encode(null)).isNull();
|
||||||
boolean result = encoder.matches(PASSWORD, "invalid-hash-format");
|
}
|
||||||
|
|
||||||
assertThat(result).isFalse();
|
@Test
|
||||||
|
void multipleEncodesProduceDifferentHashesButAllMatch() {
|
||||||
|
BcryptPassword4jPasswordEncoder encoder = new BcryptPassword4jPasswordEncoder(BcryptFunction.getInstance(4));
|
||||||
|
|
||||||
|
String encoded1 = encoder.encode(PASSWORD);
|
||||||
|
String encoded2 = encoder.encode(PASSWORD);
|
||||||
|
// Bcrypt should produce different salted hashes for the same raw password
|
||||||
|
assertThat(encoded1).isNotEqualTo(encoded2);
|
||||||
|
assertThat(encoder.matches(PASSWORD, encoded1)).isTrue();
|
||||||
|
assertThat(encoder.matches(PASSWORD, encoded2)).isTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void upgradeEncodingShouldReturnFalse() {
|
void upgradeEncodingShouldReturnFalse() {
|
||||||
HashingFunction hashingFunction = BcryptFunction.getInstance(4);
|
BcryptPassword4jPasswordEncoder encoder = new BcryptPassword4jPasswordEncoder(BcryptFunction.getInstance(4));
|
||||||
Password4jPasswordEncoder encoder = new Password4jPasswordEncoder(hashingFunction);
|
|
||||||
|
|
||||||
String encoded = encoder.encode(PASSWORD);
|
String encoded = encoder.encode(PASSWORD);
|
||||||
boolean result = encoder.upgradeEncoding(encoded);
|
boolean result = encoder.upgradeEncoding(encoded);
|
||||||
@ -110,32 +92,14 @@ class Password4jPasswordEncoderTests {
|
|||||||
assertThat(result).isFalse();
|
assertThat(result).isFalse();
|
||||||
}
|
}
|
||||||
|
|
||||||
// AlgorithmFinder Sanity Check Tests
|
|
||||||
@Test
|
@Test
|
||||||
void algorithmFinderBcryptSanityCheck() {
|
void matchesShouldReturnFalseWhenRawOrEncodedNullOrEmpty() {
|
||||||
Password4jPasswordEncoder encoder = new Password4jPasswordEncoder(AlgorithmFinder.getBcryptInstance());
|
BcryptPassword4jPasswordEncoder encoder = new BcryptPassword4jPasswordEncoder(BcryptFunction.getInstance(4));
|
||||||
|
|
||||||
String encoded = encoder.encode(PASSWORD);
|
String encoded = encoder.encode(PASSWORD);
|
||||||
assertThat(encoder.matches(PASSWORD, encoded)).isTrue();
|
assertThat(encoder.matches(null, encoded)).isFalse();
|
||||||
assertThat(encoder.matches(WRONG_PASSWORD, encoded)).isFalse();
|
assertThat(encoder.matches("", encoded)).isFalse();
|
||||||
}
|
assertThat(encoder.matches(PASSWORD, null)).isFalse();
|
||||||
|
assertThat(encoder.matches(PASSWORD, "")).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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -16,24 +16,21 @@
|
|||||||
|
|
||||||
package org.springframework.security.crypto.password4j;
|
package org.springframework.security.crypto.password4j;
|
||||||
|
|
||||||
import com.password4j.AlgorithmFinder;
|
|
||||||
import com.password4j.Argon2Function;
|
import com.password4j.Argon2Function;
|
||||||
import com.password4j.BcryptFunction;
|
import com.password4j.BcryptFunction;
|
||||||
import com.password4j.CompressedPBKDF2Function;
|
|
||||||
import com.password4j.ScryptFunction;
|
import com.password4j.ScryptFunction;
|
||||||
import com.password4j.types.Argon2;
|
import com.password4j.types.Argon2;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import org.springframework.security.crypto.argon2.Argon2PasswordEncoder;
|
import org.springframework.security.crypto.argon2.Argon2PasswordEncoder;
|
||||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||||
import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder;
|
|
||||||
import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder;
|
import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests compatibility between existing Spring Security password encoders and
|
* Tests compatibility between existing Spring Security password encoders and
|
||||||
* {@link Password4jPasswordEncoder}.
|
* Password4j-based password encoders.
|
||||||
*
|
*
|
||||||
* @author Mehrdad Bozorgmehr
|
* @author Mehrdad Bozorgmehr
|
||||||
*/
|
*/
|
||||||
@ -45,7 +42,8 @@ class PasswordCompatibilityTests {
|
|||||||
@Test
|
@Test
|
||||||
void bcryptEncodedWithSpringSecurityShouldMatchWithPassword4j() {
|
void bcryptEncodedWithSpringSecurityShouldMatchWithPassword4j() {
|
||||||
BCryptPasswordEncoder springEncoder = new BCryptPasswordEncoder(10);
|
BCryptPasswordEncoder springEncoder = new BCryptPasswordEncoder(10);
|
||||||
Password4jPasswordEncoder password4jEncoder = new Password4jPasswordEncoder(BcryptFunction.getInstance(10));
|
BcryptPassword4jPasswordEncoder password4jEncoder = new BcryptPassword4jPasswordEncoder(
|
||||||
|
BcryptFunction.getInstance(10));
|
||||||
|
|
||||||
String encodedBySpring = springEncoder.encode(PASSWORD);
|
String encodedBySpring = springEncoder.encode(PASSWORD);
|
||||||
boolean matchedByPassword4j = password4jEncoder.matches(PASSWORD, encodedBySpring);
|
boolean matchedByPassword4j = password4jEncoder.matches(PASSWORD, encodedBySpring);
|
||||||
@ -54,9 +52,35 @@ class PasswordCompatibilityTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void bcryptEncodedWithPassword4jShouldMatchWithSpringSecurity() {
|
void bcryptEncodedWithPassword4jShouldMatchWithSpringSecirity() {
|
||||||
|
BcryptPassword4jPasswordEncoder password4jEncoder = new BcryptPassword4jPasswordEncoder(
|
||||||
|
BcryptFunction.getInstance(10));
|
||||||
BCryptPasswordEncoder springEncoder = new BCryptPasswordEncoder(10);
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Argon2 Compatibility Tests
|
||||||
|
@Test
|
||||||
|
void argon2EncodedWithSpringSecurityShouldMatchWithPassword4j() {
|
||||||
|
Argon2PasswordEncoder springEncoder = new Argon2PasswordEncoder(16, 32, 1, 4096, 3);
|
||||||
|
Argon2Password4jPasswordEncoder password4jEncoder = new Argon2Password4jPasswordEncoder(
|
||||||
|
Argon2Function.getInstance(4096, 3, 1, 32, Argon2.ID));
|
||||||
|
|
||||||
|
String encodedBySpring = springEncoder.encode(PASSWORD);
|
||||||
|
boolean matchedByPassword4j = password4jEncoder.matches(PASSWORD, encodedBySpring);
|
||||||
|
|
||||||
|
assertThat(matchedByPassword4j).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void argon2EncodedWithPassword4jShouldMatchWithSpringSecirity() {
|
||||||
|
Argon2Password4jPasswordEncoder password4jEncoder = new Argon2Password4jPasswordEncoder(
|
||||||
|
Argon2Function.getInstance(4096, 3, 1, 32, Argon2.ID));
|
||||||
|
Argon2PasswordEncoder springEncoder = new Argon2PasswordEncoder(16, 32, 1, 4096, 3);
|
||||||
|
|
||||||
String encodedByPassword4j = password4jEncoder.encode(PASSWORD);
|
String encodedByPassword4j = password4jEncoder.encode(PASSWORD);
|
||||||
boolean matchedBySpring = springEncoder.matches(PASSWORD, encodedByPassword4j);
|
boolean matchedBySpring = springEncoder.matches(PASSWORD, encodedByPassword4j);
|
||||||
@ -68,7 +92,7 @@ class PasswordCompatibilityTests {
|
|||||||
@Test
|
@Test
|
||||||
void scryptEncodedWithSpringSecurityShouldMatchWithPassword4j() {
|
void scryptEncodedWithSpringSecurityShouldMatchWithPassword4j() {
|
||||||
SCryptPasswordEncoder springEncoder = new SCryptPasswordEncoder(16384, 8, 1, 32, 64);
|
SCryptPasswordEncoder springEncoder = new SCryptPasswordEncoder(16384, 8, 1, 32, 64);
|
||||||
Password4jPasswordEncoder password4jEncoder = new Password4jPasswordEncoder(
|
ScryptPassword4jPasswordEncoder password4jEncoder = new ScryptPassword4jPasswordEncoder(
|
||||||
ScryptFunction.getInstance(16384, 8, 1, 32));
|
ScryptFunction.getInstance(16384, 8, 1, 32));
|
||||||
|
|
||||||
String encodedBySpring = springEncoder.encode(PASSWORD);
|
String encodedBySpring = springEncoder.encode(PASSWORD);
|
||||||
@ -78,10 +102,10 @@ class PasswordCompatibilityTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void scryptEncodedWithPassword4jShouldMatchWithSpringSecurity() {
|
void scryptEncodedWithPassword4jShouldMatchWithSpringSecirity() {
|
||||||
|
ScryptPassword4jPasswordEncoder password4jEncoder = new ScryptPassword4jPasswordEncoder(
|
||||||
|
ScryptFunction.getInstance(16384, 8, 1, 32));
|
||||||
SCryptPasswordEncoder springEncoder = new SCryptPasswordEncoder(16384, 8, 1, 32, 64);
|
SCryptPasswordEncoder springEncoder = new SCryptPasswordEncoder(16384, 8, 1, 32, 64);
|
||||||
Password4jPasswordEncoder password4jEncoder = new Password4jPasswordEncoder(
|
|
||||||
ScryptFunction.getInstance(16384, 8, 1, 32));
|
|
||||||
|
|
||||||
String encodedByPassword4j = password4jEncoder.encode(PASSWORD);
|
String encodedByPassword4j = password4jEncoder.encode(PASSWORD);
|
||||||
boolean matchedBySpring = springEncoder.matches(PASSWORD, encodedByPassword4j);
|
boolean matchedBySpring = springEncoder.matches(PASSWORD, encodedByPassword4j);
|
||||||
@ -89,72 +113,4 @@ class PasswordCompatibilityTests {
|
|||||||
assertThat(matchedBySpring).isTrue();
|
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,248 @@
|
|||||||
|
/*
|
||||||
|
* 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.ScryptFunction;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.CsvSource;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link ScryptPassword4jPasswordEncoder}.
|
||||||
|
*
|
||||||
|
* @author Mehrdad Bozorgmehr
|
||||||
|
*/
|
||||||
|
class ScryptPassword4jPasswordEncoderTests {
|
||||||
|
|
||||||
|
private static final String PASSWORD = "password";
|
||||||
|
|
||||||
|
private static final String LONG_PASSWORD = "a".repeat(1000);
|
||||||
|
|
||||||
|
private static final String SPECIAL_CHARS_PASSWORD = "p@ssw0rd!#$%^&*()_+-=[]{}|;':\",./<>?";
|
||||||
|
|
||||||
|
private static final String UNICODE_PASSWORD = "пароль密码パスワード🔐";
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void defaultConstructorShouldCreateWorkingEncoder() {
|
||||||
|
ScryptPassword4jPasswordEncoder encoder = new ScryptPassword4jPasswordEncoder();
|
||||||
|
|
||||||
|
String encoded = encoder.encode(PASSWORD);
|
||||||
|
|
||||||
|
assertThat(encoded).isNotNull();
|
||||||
|
// Password4j scrypt format differs from classic $s0$; accept generic multi-part
|
||||||
|
// format
|
||||||
|
assertThat(encoded.split("\\$").length).isGreaterThanOrEqualTo(3);
|
||||||
|
assertThat(encoder.matches(PASSWORD, encoded)).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void constructorWithNullScryptFunctionShouldThrowException() {
|
||||||
|
assertThatIllegalArgumentException().isThrownBy(() -> new ScryptPassword4jPasswordEncoder(null))
|
||||||
|
.withMessage("hashingFunction cannot be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void constructorWithCustomScryptFunctionShouldWork() {
|
||||||
|
ScryptFunction customFunction = ScryptFunction.getInstance(16384, 8, 1, 32);
|
||||||
|
ScryptPassword4jPasswordEncoder encoder = new ScryptPassword4jPasswordEncoder(customFunction);
|
||||||
|
|
||||||
|
String encoded = encoder.encode(PASSWORD);
|
||||||
|
|
||||||
|
assertThat(encoded).isNotNull();
|
||||||
|
assertThat(encoded.split("\\$").length).isGreaterThanOrEqualTo(3);
|
||||||
|
assertThat(encoder.matches(PASSWORD, encoded)).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@CsvSource({ "1024, 8, 1, 16", "4096, 8, 1, 32", "16384, 8, 1, 32", "32768, 8, 1, 64" })
|
||||||
|
void encodingShouldWorkWithDifferentParameters(int N, int r, int p, int dkLen) {
|
||||||
|
ScryptFunction function = ScryptFunction.getInstance(N, r, p, dkLen);
|
||||||
|
ScryptPassword4jPasswordEncoder encoder = new ScryptPassword4jPasswordEncoder(function);
|
||||||
|
|
||||||
|
String encoded = encoder.encode(PASSWORD);
|
||||||
|
|
||||||
|
assertThat(encoded).isNotNull();
|
||||||
|
assertThat(encoded.split("\\$").length).isGreaterThanOrEqualTo(3);
|
||||||
|
assertThat(encoder.matches(PASSWORD, encoded)).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void encodingShouldGenerateDifferentHashesForSamePassword() {
|
||||||
|
ScryptPassword4jPasswordEncoder encoder = new ScryptPassword4jPasswordEncoder();
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldHandleLongPasswords() {
|
||||||
|
ScryptPassword4jPasswordEncoder encoder = new ScryptPassword4jPasswordEncoder();
|
||||||
|
|
||||||
|
String encoded = encoder.encode(LONG_PASSWORD);
|
||||||
|
|
||||||
|
assertThat(encoder.matches(LONG_PASSWORD, encoded)).isTrue();
|
||||||
|
assertThat(encoder.matches("wrong", encoded)).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldHandleSpecialCharacters() {
|
||||||
|
ScryptPassword4jPasswordEncoder encoder = new ScryptPassword4jPasswordEncoder();
|
||||||
|
|
||||||
|
String encoded = encoder.encode(SPECIAL_CHARS_PASSWORD);
|
||||||
|
|
||||||
|
assertThat(encoder.matches(SPECIAL_CHARS_PASSWORD, encoded)).isTrue();
|
||||||
|
assertThat(encoder.matches("wrong", encoded)).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldHandleUnicodeCharacters() {
|
||||||
|
ScryptPassword4jPasswordEncoder encoder = new ScryptPassword4jPasswordEncoder();
|
||||||
|
|
||||||
|
String encoded = encoder.encode(UNICODE_PASSWORD);
|
||||||
|
|
||||||
|
assertThat(encoder.matches(UNICODE_PASSWORD, encoded)).isTrue();
|
||||||
|
assertThat(encoder.matches("wrong", encoded)).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRejectIncorrectPasswords() {
|
||||||
|
ScryptPassword4jPasswordEncoder encoder = new ScryptPassword4jPasswordEncoder();
|
||||||
|
String encoded = encoder.encode(PASSWORD);
|
||||||
|
|
||||||
|
assertThat(encoder.matches("wrongpassword", encoded)).isFalse();
|
||||||
|
assertThat(encoder.matches("PASSWORD", encoded)).isFalse(); // Case sensitive
|
||||||
|
assertThat(encoder.matches("password ", encoded)).isFalse(); // Trailing space
|
||||||
|
assertThat(encoder.matches(" password", encoded)).isFalse(); // Leading space
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void matchesShouldReturnFalseForNullOrEmptyInputs() {
|
||||||
|
ScryptPassword4jPasswordEncoder encoder = new ScryptPassword4jPasswordEncoder();
|
||||||
|
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();
|
||||||
|
assertThat(encoder.matches(null, null)).isFalse();
|
||||||
|
assertThat(encoder.matches("", "")).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void encodeNullShouldReturnNull() {
|
||||||
|
ScryptPassword4jPasswordEncoder encoder = new ScryptPassword4jPasswordEncoder();
|
||||||
|
|
||||||
|
assertThat(encoder.encode(null)).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void upgradeEncodingShouldReturnFalse() {
|
||||||
|
ScryptPassword4jPasswordEncoder encoder = new ScryptPassword4jPasswordEncoder();
|
||||||
|
String encoded = encoder.encode(PASSWORD);
|
||||||
|
|
||||||
|
assertThat(encoder.upgradeEncoding(encoded)).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldWorkWithAlgorithmFinderDefaults() {
|
||||||
|
ScryptPassword4jPasswordEncoder encoder = new ScryptPassword4jPasswordEncoder(
|
||||||
|
AlgorithmFinder.getScryptInstance());
|
||||||
|
|
||||||
|
String encoded = encoder.encode(PASSWORD);
|
||||||
|
|
||||||
|
assertThat(encoded).isNotNull();
|
||||||
|
assertThat(encoder.matches(PASSWORD, encoded)).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRejectMalformedHashes() {
|
||||||
|
ScryptPassword4jPasswordEncoder encoder = new ScryptPassword4jPasswordEncoder();
|
||||||
|
assertMalformedRejected(encoder, PASSWORD, "invalid_hash");
|
||||||
|
assertMalformedRejected(encoder, PASSWORD, "$s0$invalid");
|
||||||
|
assertMalformedRejected(encoder, PASSWORD, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertMalformedRejected(ScryptPassword4jPasswordEncoder encoder, String raw, String malformed) {
|
||||||
|
boolean rejected;
|
||||||
|
try {
|
||||||
|
rejected = !encoder.matches(raw, malformed);
|
||||||
|
}
|
||||||
|
catch (RuntimeException ex) {
|
||||||
|
rejected = true; // exception path acceptable
|
||||||
|
}
|
||||||
|
assertThat(rejected).as("Malformed hash should not validate: " + malformed).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldHandleEmptyStringPassword() {
|
||||||
|
ScryptPassword4jPasswordEncoder encoder = new ScryptPassword4jPasswordEncoder();
|
||||||
|
String encoded = encoder.encode("");
|
||||||
|
assertThat(encoded).isNotNull();
|
||||||
|
boolean emptyMatches;
|
||||||
|
try {
|
||||||
|
emptyMatches = encoder.matches("", encoded);
|
||||||
|
}
|
||||||
|
catch (RuntimeException ex) {
|
||||||
|
emptyMatches = false;
|
||||||
|
}
|
||||||
|
if (emptyMatches) {
|
||||||
|
assertThat(encoder.matches("", encoded)).isTrue();
|
||||||
|
}
|
||||||
|
assertThat(encoder.matches("notEmpty", encoded)).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldHandleCustomCostParameters() {
|
||||||
|
// Test with low cost parameters for speed
|
||||||
|
ScryptFunction lowCost = ScryptFunction.getInstance(1024, 1, 1, 16);
|
||||||
|
// Test with higher cost parameters
|
||||||
|
ScryptFunction highCost = ScryptFunction.getInstance(32768, 8, 2, 64);
|
||||||
|
|
||||||
|
ScryptPassword4jPasswordEncoder lowEncoder = new ScryptPassword4jPasswordEncoder(lowCost);
|
||||||
|
ScryptPassword4jPasswordEncoder highEncoder = new ScryptPassword4jPasswordEncoder(highCost);
|
||||||
|
|
||||||
|
String lowEncoded = lowEncoder.encode(PASSWORD);
|
||||||
|
String highEncoded = highEncoder.encode(PASSWORD);
|
||||||
|
|
||||||
|
assertThat(lowEncoder.matches(PASSWORD, lowEncoded)).isTrue();
|
||||||
|
assertThat(highEncoder.matches(PASSWORD, highEncoded)).isTrue();
|
||||||
|
|
||||||
|
// Each encoder should work with hashes generated by the same parameters
|
||||||
|
assertThat(lowEncoded).isNotEqualTo(highEncoded);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldHandleEdgeCaseParameters() {
|
||||||
|
// Test with minimum practical parameters
|
||||||
|
ScryptFunction minParams = ScryptFunction.getInstance(2, 1, 1, 1);
|
||||||
|
ScryptPassword4jPasswordEncoder encoder = new ScryptPassword4jPasswordEncoder(minParams);
|
||||||
|
|
||||||
|
String encoded = encoder.encode(PASSWORD);
|
||||||
|
|
||||||
|
assertThat(encoded).isNotNull();
|
||||||
|
assertThat(encoder.matches(PASSWORD, encoded)).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user