mirror of
https://github.com/spring-projects/spring-security.git
synced 2025-10-23 10:48:51 +00:00
Add password4j implementation of PasswordEncoder
This commit is contained in:
commit
5ca18a3b9c
@ -8,6 +8,7 @@ dependencies {
|
|||||||
management platform(project(":spring-security-dependencies"))
|
management platform(project(":spring-security-dependencies"))
|
||||||
optional 'org.springframework:spring-core'
|
optional 'org.springframework:spring-core'
|
||||||
optional 'org.bouncycastle:bcpkix-jdk18on'
|
optional 'org.bouncycastle:bcpkix-jdk18on'
|
||||||
|
optional libs.com.password4j.password4j
|
||||||
|
|
||||||
testImplementation "org.assertj:assertj-core"
|
testImplementation "org.assertj:assertj-core"
|
||||||
testImplementation "org.junit.jupiter:junit-jupiter-api"
|
testImplementation "org.junit.jupiter:junit-jupiter-api"
|
||||||
|
@ -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,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,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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,78 @@
|
|||||||
|
/*
|
||||||
|
* 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.Hash;
|
||||||
|
import com.password4j.HashingFunction;
|
||||||
|
import com.password4j.Password;
|
||||||
|
|
||||||
|
import org.springframework.security.crypto.password.AbstractValidatingPasswordEncoder;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract base class for Password4j-based password encoders. This class provides the
|
||||||
|
* common functionality for password encoding and verification using the Password4j
|
||||||
|
* library.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This class is package-private and should not be used directly. Instead, use the
|
||||||
|
* specific public subclasses that support verified hashing algorithms such as BCrypt,
|
||||||
|
* Argon2, and SCrypt implementations.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This implementation is thread-safe and can be shared across multiple threads.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @author Mehrdad Bozorgmehr
|
||||||
|
* @since 7.0
|
||||||
|
*/
|
||||||
|
abstract class Password4jPasswordEncoder extends AbstractValidatingPasswordEncoder {
|
||||||
|
|
||||||
|
private final HashingFunction hashingFunction;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a Password4j password encoder with the specified hashing function. This
|
||||||
|
* constructor is package-private and intended for use by subclasses only.
|
||||||
|
* @param hashingFunction the hashing function to use for encoding passwords, must not
|
||||||
|
* be null
|
||||||
|
* @throws IllegalArgumentException if hashingFunction is null
|
||||||
|
*/
|
||||||
|
Password4jPasswordEncoder(HashingFunction hashingFunction) {
|
||||||
|
Assert.notNull(hashingFunction, "hashingFunction cannot be null");
|
||||||
|
this.hashingFunction = hashingFunction;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String encodeNonNullPassword(String rawPassword) {
|
||||||
|
Hash hash = Password.hash(rawPassword).with(this.hashingFunction);
|
||||||
|
return hash.getResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean matchesNonNull(String rawPassword, String encodedPassword) {
|
||||||
|
return Password.check(rawPassword, encodedPassword).with(this.hashingFunction);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean upgradeEncodingNonNull(String encodedPassword) {
|
||||||
|
// Password4j handles upgrade detection internally for most algorithms
|
||||||
|
// For now, we'll return false to maintain existing behavior
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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,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,20 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2004-present the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@NullMarked
|
||||||
|
package org.springframework.security.crypto.password4j;
|
||||||
|
|
||||||
|
import org.jspecify.annotations.NullMarked;
|
@ -0,0 +1,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,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,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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,105 @@
|
|||||||
|
/*
|
||||||
|
* 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.BcryptFunction;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base functionality tests for {@link Password4jPasswordEncoder} implementations. These
|
||||||
|
* tests verify the common behavior across all concrete password encoder subclasses.
|
||||||
|
*
|
||||||
|
* @author Mehrdad Bozorgmehr
|
||||||
|
*/
|
||||||
|
class Password4jPasswordEncoderTests {
|
||||||
|
|
||||||
|
private static final String PASSWORD = "password";
|
||||||
|
|
||||||
|
private static final String WRONG_PASSWORD = "wrongpassword";
|
||||||
|
|
||||||
|
// Test abstract class behavior through concrete implementation
|
||||||
|
@Test
|
||||||
|
void encodeShouldReturnNonNullHashedPassword() {
|
||||||
|
BcryptPassword4jPasswordEncoder encoder = new BcryptPassword4jPasswordEncoder(BcryptFunction.getInstance(4));
|
||||||
|
|
||||||
|
String result = encoder.encode(PASSWORD);
|
||||||
|
|
||||||
|
assertThat(result).isNotNull().isNotEqualTo(PASSWORD);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void matchesShouldReturnTrueForValidPassword() {
|
||||||
|
BcryptPassword4jPasswordEncoder encoder = new BcryptPassword4jPasswordEncoder(BcryptFunction.getInstance(4));
|
||||||
|
|
||||||
|
String encoded = encoder.encode(PASSWORD);
|
||||||
|
boolean result = encoder.matches(PASSWORD, encoded);
|
||||||
|
|
||||||
|
assertThat(result).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void matchesShouldReturnFalseForInvalidPassword() {
|
||||||
|
BcryptPassword4jPasswordEncoder encoder = new BcryptPassword4jPasswordEncoder(BcryptFunction.getInstance(4));
|
||||||
|
|
||||||
|
String encoded = encoder.encode(PASSWORD);
|
||||||
|
boolean result = encoder.matches(WRONG_PASSWORD, encoded);
|
||||||
|
|
||||||
|
assertThat(result).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void encodeNullPasswordShouldReturnNull() {
|
||||||
|
BcryptPassword4jPasswordEncoder encoder = new BcryptPassword4jPasswordEncoder(BcryptFunction.getInstance(4));
|
||||||
|
|
||||||
|
assertThat(encoder.encode(null)).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@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
|
||||||
|
void upgradeEncodingShouldReturnFalse() {
|
||||||
|
BcryptPassword4jPasswordEncoder encoder = new BcryptPassword4jPasswordEncoder(BcryptFunction.getInstance(4));
|
||||||
|
|
||||||
|
String encoded = encoder.encode(PASSWORD);
|
||||||
|
boolean result = encoder.upgradeEncoding(encoded);
|
||||||
|
|
||||||
|
assertThat(result).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void matchesShouldReturnFalseWhenRawOrEncodedNullOrEmpty() {
|
||||||
|
BcryptPassword4jPasswordEncoder encoder = new BcryptPassword4jPasswordEncoder(BcryptFunction.getInstance(4));
|
||||||
|
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,153 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2004-present the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.crypto.password4j;
|
||||||
|
|
||||||
|
import com.password4j.Argon2Function;
|
||||||
|
import com.password4j.BcryptFunction;
|
||||||
|
import com.password4j.ScryptFunction;
|
||||||
|
import com.password4j.types.Argon2;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import org.springframework.security.crypto.argon2.Argon2PasswordEncoder;
|
||||||
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||||
|
import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder;
|
||||||
|
import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests compatibility between existing Spring Security password encoders and
|
||||||
|
* Password4j-based password encoders.
|
||||||
|
*
|
||||||
|
* @author Mehrdad Bozorgmehr
|
||||||
|
*/
|
||||||
|
class PasswordCompatibilityTests {
|
||||||
|
|
||||||
|
private static final String PASSWORD = "password";
|
||||||
|
|
||||||
|
// BCrypt Compatibility Tests
|
||||||
|
@Test
|
||||||
|
void bcryptEncodedWithSpringSecurityShouldMatchWithPassword4j() {
|
||||||
|
BCryptPasswordEncoder springEncoder = new BCryptPasswordEncoder(10);
|
||||||
|
BcryptPassword4jPasswordEncoder password4jEncoder = new BcryptPassword4jPasswordEncoder(
|
||||||
|
BcryptFunction.getInstance(10));
|
||||||
|
|
||||||
|
String encodedBySpring = springEncoder.encode(PASSWORD);
|
||||||
|
boolean matchedByPassword4j = password4jEncoder.matches(PASSWORD, encodedBySpring);
|
||||||
|
|
||||||
|
assertThat(matchedByPassword4j).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void bcryptEncodedWithPassword4jShouldMatchWithSpringSecurity() {
|
||||||
|
BcryptPassword4jPasswordEncoder password4jEncoder = new BcryptPassword4jPasswordEncoder(
|
||||||
|
BcryptFunction.getInstance(10));
|
||||||
|
BCryptPasswordEncoder springEncoder = new BCryptPasswordEncoder(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 argon2EncodedWithPassword4jShouldMatchWithSpringSecurity() {
|
||||||
|
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);
|
||||||
|
boolean matchedBySpring = springEncoder.matches(PASSWORD, encodedByPassword4j);
|
||||||
|
|
||||||
|
assertThat(matchedBySpring).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
// SCrypt Compatibility Tests
|
||||||
|
@Test
|
||||||
|
void scryptEncodedWithSpringSecurityShouldMatchWithPassword4j() {
|
||||||
|
SCryptPasswordEncoder springEncoder = new SCryptPasswordEncoder(16384, 8, 1, 32, 64);
|
||||||
|
ScryptPassword4jPasswordEncoder password4jEncoder = new ScryptPassword4jPasswordEncoder(
|
||||||
|
ScryptFunction.getInstance(16384, 8, 1, 32));
|
||||||
|
|
||||||
|
String encodedBySpring = springEncoder.encode(PASSWORD);
|
||||||
|
boolean matchedByPassword4j = password4jEncoder.matches(PASSWORD, encodedBySpring);
|
||||||
|
|
||||||
|
assertThat(matchedByPassword4j).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void scryptEncodedWithPassword4jShouldMatchWithSpringSecurity() {
|
||||||
|
ScryptPassword4jPasswordEncoder password4jEncoder = new ScryptPassword4jPasswordEncoder(
|
||||||
|
ScryptFunction.getInstance(16384, 8, 1, 32));
|
||||||
|
SCryptPasswordEncoder springEncoder = new SCryptPasswordEncoder(16384, 8, 1, 32, 64);
|
||||||
|
|
||||||
|
String encodedByPassword4j = password4jEncoder.encode(PASSWORD);
|
||||||
|
boolean matchedBySpring = springEncoder.matches(PASSWORD, encodedByPassword4j);
|
||||||
|
|
||||||
|
assertThat(matchedBySpring).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
// PBKDF2 Compatibility Tests
|
||||||
|
@Test
|
||||||
|
void pbkdf2EncodedWithSpringSecurityCannotMatchWithPassword4j() {
|
||||||
|
// Note: Direct compatibility between Spring Security's Pbkdf2PasswordEncoder
|
||||||
|
// and Password4j's PBKDF2 implementation is not possible because they use
|
||||||
|
// different output formats. Spring Security uses hex encoding with a specific
|
||||||
|
// format,
|
||||||
|
// while our Password4jPasswordEncoder uses salt:hash format with Base64 encoding.
|
||||||
|
Pbkdf2PasswordEncoder springEncoder = Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8();
|
||||||
|
Pbkdf2Password4jPasswordEncoder password4jEncoder = new Pbkdf2Password4jPasswordEncoder();
|
||||||
|
|
||||||
|
String encodedBySpring = springEncoder.encode(PASSWORD);
|
||||||
|
String encodedByPassword4j = password4jEncoder.encode(PASSWORD);
|
||||||
|
|
||||||
|
// These should NOT match due to different formats
|
||||||
|
// Spring Security will throw an exception when trying to decode Password4j
|
||||||
|
// format,
|
||||||
|
// which should be treated as a non-match
|
||||||
|
boolean password4jCanMatchSpring = password4jEncoder.matches(PASSWORD, encodedBySpring);
|
||||||
|
boolean springCanMatchPassword4j;
|
||||||
|
try {
|
||||||
|
springCanMatchPassword4j = springEncoder.matches(PASSWORD, encodedByPassword4j);
|
||||||
|
}
|
||||||
|
catch (IllegalArgumentException ex) {
|
||||||
|
// Expected exception due to format incompatibility - treat as non-match
|
||||||
|
springCanMatchPassword4j = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
assertThat(password4jCanMatchSpring).isFalse();
|
||||||
|
assertThat(springCanMatchPassword4j).isFalse();
|
||||||
|
|
||||||
|
// But each should match its own encoding
|
||||||
|
assertThat(springEncoder.matches(PASSWORD, encodedBySpring)).isTrue();
|
||||||
|
assertThat(password4jEncoder.matches(PASSWORD, encodedByPassword4j)).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -78,6 +78,6 @@ dependencies {
|
|||||||
api libs.org.apache.maven.resolver.maven.resolver.transport.http
|
api libs.org.apache.maven.resolver.maven.resolver.transport.http
|
||||||
api libs.org.apache.maven.maven.resolver.provider
|
api libs.org.apache.maven.maven.resolver.provider
|
||||||
api libs.org.instancio.instancio.junit
|
api libs.org.instancio.instancio.junit
|
||||||
|
api libs.com.password4j.password4j
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -463,6 +463,115 @@ There are a significant number of other `PasswordEncoder` implementations that e
|
|||||||
They are all deprecated to indicate that they are no longer considered secure.
|
They are all deprecated to indicate that they are no longer considered secure.
|
||||||
However, there are no plans to remove them, since it is difficult to migrate existing legacy systems.
|
However, there are no plans to remove them, since it is difficult to migrate existing legacy systems.
|
||||||
|
|
||||||
|
[[password4j]]
|
||||||
|
== Password4j-based Password Encoders
|
||||||
|
|
||||||
|
Spring Security 7.0 introduces alternative password encoder implementations based on the https://github.com/Password4j/password4j[Password4j] library.
|
||||||
|
These encoders provide additional options for popular hashing algorithms and can be used as alternatives to the existing Spring Security implementations.
|
||||||
|
|
||||||
|
The Password4j library is a Java cryptographic library that focuses on password hashing with support for multiple algorithms.
|
||||||
|
These encoders are particularly useful when you need specific algorithm configurations or want to leverage Password4j's optimizations.
|
||||||
|
|
||||||
|
All Password4j-based encoders are thread-safe and can be shared across multiple threads.
|
||||||
|
|
||||||
|
[[password4j-argon2]]
|
||||||
|
=== Argon2Password4jPasswordEncoder
|
||||||
|
|
||||||
|
The `Argon2Password4jPasswordEncoder` implementation uses the https://en.wikipedia.org/wiki/Argon2[Argon2] algorithm via the Password4j library to hash passwords.
|
||||||
|
This provides an alternative to Spring Security's built-in `Argon2PasswordEncoder` with different configuration options and potential performance characteristics.
|
||||||
|
|
||||||
|
Argon2 is the winner of the https://en.wikipedia.org/wiki/Password_Hashing_Competition[Password Hashing Competition] and is recommended for new applications.
|
||||||
|
This implementation leverages Password4j's Argon2 support which properly includes the salt in the output hash.
|
||||||
|
|
||||||
|
Create an encoder with default settings:
|
||||||
|
|
||||||
|
.Argon2Password4jPasswordEncoder
|
||||||
|
include-code::./Argon2UsageTests[tag=default-params,indent=0]
|
||||||
|
|
||||||
|
Create an encoder with custom Argon2 parameters:
|
||||||
|
|
||||||
|
.Argon2Password4jPasswordEncoder Custom
|
||||||
|
include-code::./Argon2UsageTests[tag=custom-params,indent=0]
|
||||||
|
|
||||||
|
[[password4j-bcrypt]]
|
||||||
|
=== BcryptPassword4jPasswordEncoder
|
||||||
|
|
||||||
|
The `BcryptPassword4jPasswordEncoder` implementation uses the https://en.wikipedia.org/wiki/Bcrypt[BCrypt] algorithm via the Password4j library to hash passwords.
|
||||||
|
This provides an alternative to Spring Security's built-in `BCryptPasswordEncoder` with Password4j's implementation characteristics.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Create an encoder with default settings:
|
||||||
|
|
||||||
|
.BcryptPassword4jPasswordEncoder
|
||||||
|
include-code::./BcryptUsageTests[tag=default-params,indent=0]
|
||||||
|
|
||||||
|
Create an encoder with custom bcrypt parameters:
|
||||||
|
|
||||||
|
.BcryptPassword4jPasswordEncoder Custom
|
||||||
|
include-code::./BcryptUsageTests[tag=custom-params,indent=0]
|
||||||
|
|
||||||
|
[[password4j-scrypt]]
|
||||||
|
=== ScryptPassword4jPasswordEncoder
|
||||||
|
|
||||||
|
The `ScryptPassword4jPasswordEncoder` implementation uses the https://en.wikipedia.org/wiki/Scrypt[SCrypt] algorithm via the Password4j library to hash passwords.
|
||||||
|
This provides an alternative to Spring Security's built-in `SCryptPasswordEncoder` with Password4j's implementation characteristics.
|
||||||
|
|
||||||
|
SCrypt is a memory-hard password hashing algorithm designed to be resistant to hardware brute-force attacks.
|
||||||
|
This implementation leverages Password4j's SCrypt support which properly includes the salt in the output hash.
|
||||||
|
|
||||||
|
|
||||||
|
Create an encoder with default settings:
|
||||||
|
|
||||||
|
.ScryptPassword4jPasswordEncoder
|
||||||
|
include-code::./ScryptUsageTests[tag=default-params,indent=0]
|
||||||
|
|
||||||
|
Create an encoder with custom scrypt parameters:
|
||||||
|
|
||||||
|
.ScryptPassword4jPasswordEncoder Custom
|
||||||
|
include-code::./ScryptUsageTests[tag=custom-params,indent=0]
|
||||||
|
|
||||||
|
[[password4j-pbkdf2]]
|
||||||
|
=== Pbkdf2Password4jPasswordEncoder
|
||||||
|
|
||||||
|
The `Pbkdf2Password4jPasswordEncoder` implementation uses the https://en.wikipedia.org/wiki/PBKDF2[PBKDF2] algorithm via the Password4j library to hash passwords.
|
||||||
|
This provides an alternative to Spring Security's built-in `Pbkdf2PasswordEncoder` with explicit salt management.
|
||||||
|
|
||||||
|
PBKDF2 is a key derivation function designed to be computationally expensive to thwart dictionary and brute force attacks.
|
||||||
|
This implementation handles salt management explicitly since Password4j's PBKDF2 implementation does not include the salt in the output hash.
|
||||||
|
The encoded password format is: `+{salt}:{hash}+` where both salt and hash are Base64 encoded.
|
||||||
|
|
||||||
|
Create an encoder with default settings:
|
||||||
|
|
||||||
|
.Pbkdf2Password4jPasswordEncoder
|
||||||
|
include-code::./Pbkdf2UsageTests[tag=default-params,indent=0]
|
||||||
|
|
||||||
|
Create an encoder with custom PBKDF2 parameters:
|
||||||
|
|
||||||
|
.Pbkdf2Password4jPasswordEncoder Custom
|
||||||
|
include-code::./Pbkdf2UsageTests[tag=custom-params,indent=0]
|
||||||
|
|
||||||
|
[[password4j-ballooning]]
|
||||||
|
=== BalloonHashingPassword4jPasswordEncoder
|
||||||
|
|
||||||
|
The `BalloonHashingPassword4jPasswordEncoder` implementation uses the Balloon hashing algorithm via the Password4j library to hash passwords.
|
||||||
|
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 salt management explicitly since Password4j's Balloon hashing implementation does not include the salt in the output hash.
|
||||||
|
The encoded password format is: `+{salt}:{hash}+` where both salt and hash are Base64 encoded.
|
||||||
|
|
||||||
|
|
||||||
|
Create an encoder with default settings:
|
||||||
|
|
||||||
|
.BalloonHashingPassword4jPasswordEncoder
|
||||||
|
include-code::./BallooningHashingUsageTests[tag=default-params,indent=0]
|
||||||
|
|
||||||
|
Create an encoder with custom parameters:
|
||||||
|
|
||||||
|
.BalloonHashingPassword4jPasswordEncoder Custom
|
||||||
|
include-code::./BallooningHashingUsageTests[tag=custom-params,indent=0]
|
||||||
|
|
||||||
[[authentication-password-storage-configuration]]
|
[[authentication-password-storage-configuration]]
|
||||||
== Password Storage Configuration
|
== Password Storage Configuration
|
||||||
|
|
||||||
|
@ -35,6 +35,15 @@ Java::
|
|||||||
http.csrf((csrf) -> csrf.spa());
|
http.csrf((csrf) -> csrf.spa());
|
||||||
----
|
----
|
||||||
|
|
||||||
|
== Crypto
|
||||||
|
|
||||||
|
* Added Password4j-based password encoders providing alternative implementations for popular hashing algorithms:
|
||||||
|
** `Argon2Password4jPasswordEncoder` - xref:features/authentication/password-storage.adoc#password4j-argon2[Argon2]
|
||||||
|
** `BcryptPassword4jPasswordEncoder` - xref:features/authentication/password-storage.adoc#password4j-bcrypt[BCrypt]
|
||||||
|
** `ScryptPassword4jPasswordEncoder` - xref:features/authentication/password-storage.adoc#password4j-scrypt[SCrypt]
|
||||||
|
** `Pbkdf2Password4jPasswordEncoder` - xref:features/authentication/password-storage.adoc#password4j-pbkdf2[PBKDF2]
|
||||||
|
** `BalloonHashingPassword4jPasswordEncoder` - xref:features/authentication/password-storage.adoc#password4j-balloon[Balloon Hashing]
|
||||||
|
|
||||||
== Data
|
== Data
|
||||||
|
|
||||||
* Added support to Authorized objects for Spring Data types
|
* Added support to Authorized objects for Spring Data types
|
||||||
|
@ -39,6 +39,7 @@ dependencies {
|
|||||||
testImplementation project(':spring-security-test')
|
testImplementation project(':spring-security-test')
|
||||||
testImplementation project(':spring-security-oauth2-client')
|
testImplementation project(':spring-security-oauth2-client')
|
||||||
testImplementation 'com.squareup.okhttp3:mockwebserver'
|
testImplementation 'com.squareup.okhttp3:mockwebserver'
|
||||||
|
testImplementation libs.com.password4j.password4j
|
||||||
testImplementation 'com.unboundid:unboundid-ldapsdk'
|
testImplementation 'com.unboundid:unboundid-ldapsdk'
|
||||||
testImplementation libs.webauthn4j.core
|
testImplementation libs.webauthn4j.core
|
||||||
testImplementation 'org.jetbrains.kotlin:kotlin-reflect'
|
testImplementation 'org.jetbrains.kotlin:kotlin-reflect'
|
||||||
|
@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
* 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.docs.features.authentication.password4jargon2;
|
||||||
|
|
||||||
|
import com.password4j.Argon2Function;
|
||||||
|
import com.password4j.types.Argon2;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.security.crypto.password4j.Argon2Password4jPasswordEncoder;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Rob Winch
|
||||||
|
*/
|
||||||
|
public class Argon2UsageTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void defaultParams() {
|
||||||
|
// tag::default-params[]
|
||||||
|
PasswordEncoder encoder = new Argon2Password4jPasswordEncoder();
|
||||||
|
String result = encoder.encode("myPassword");
|
||||||
|
assertThat(encoder.matches("myPassword", result)).isTrue();
|
||||||
|
// end::default-params[]
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void customParameters() {
|
||||||
|
// tag::custom-params[]
|
||||||
|
Argon2Function argon2Fn = Argon2Function.getInstance(65536, 3, 4, 32,
|
||||||
|
Argon2.ID);
|
||||||
|
PasswordEncoder encoder = new Argon2Password4jPasswordEncoder(argon2Fn);
|
||||||
|
String result = encoder.encode("myPassword");
|
||||||
|
assertThat(encoder.matches("myPassword", result)).isTrue();
|
||||||
|
// end::custom-params[]
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
* 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.docs.features.authentication.password4jballooning;
|
||||||
|
|
||||||
|
import com.password4j.BalloonHashingFunction;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.security.crypto.password4j.BalloonHashingPassword4jPasswordEncoder;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Rob Winch
|
||||||
|
*/
|
||||||
|
public class BallooningHashingUsageTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void defaultParams() {
|
||||||
|
// tag::default-params[]
|
||||||
|
PasswordEncoder encoder = new BalloonHashingPassword4jPasswordEncoder();
|
||||||
|
String result = encoder.encode("myPassword");
|
||||||
|
assertThat(encoder.matches("myPassword", result)).isTrue();
|
||||||
|
// end::default-params[]
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void customParameters() {
|
||||||
|
// tag::custom-params[]
|
||||||
|
BalloonHashingFunction ballooningHashingFn =
|
||||||
|
BalloonHashingFunction.getInstance("SHA-256", 1024, 3, 4, 3);
|
||||||
|
PasswordEncoder encoder = new BalloonHashingPassword4jPasswordEncoder(ballooningHashingFn);
|
||||||
|
String result = encoder.encode("myPassword");
|
||||||
|
assertThat(encoder.matches("myPassword", result)).isTrue();
|
||||||
|
// end::custom-params[]
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
* 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.docs.features.authentication.password4jbcrypt;
|
||||||
|
|
||||||
|
import com.password4j.BcryptFunction;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.security.crypto.password4j.BcryptPassword4jPasswordEncoder;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Rob Winch
|
||||||
|
*/
|
||||||
|
public class BcryptUsageTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void defaultParams() {
|
||||||
|
// tag::default-params[]
|
||||||
|
PasswordEncoder encoder = new BCryptPasswordEncoder();
|
||||||
|
String result = encoder.encode("myPassword");
|
||||||
|
assertThat(encoder.matches("myPassword", result)).isTrue();
|
||||||
|
// end::default-params[]
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void customParameters() {
|
||||||
|
// tag::custom-params[]
|
||||||
|
BcryptFunction bcryptFn = BcryptFunction.getInstance(12);
|
||||||
|
PasswordEncoder encoder = new BcryptPassword4jPasswordEncoder(bcryptFn);
|
||||||
|
String result = encoder.encode("myPassword");
|
||||||
|
assertThat(encoder.matches("myPassword", result)).isTrue();
|
||||||
|
// end::custom-params[]
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
* 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.docs.features.authentication.password4jpbkdf2;
|
||||||
|
|
||||||
|
import com.password4j.PBKDF2Function;
|
||||||
|
import com.password4j.types.Hmac;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.security.crypto.password4j.Pbkdf2Password4jPasswordEncoder;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Rob Winch
|
||||||
|
*/
|
||||||
|
public class Pbkdf2UsageTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void defaultParams() {
|
||||||
|
// tag::default-params[]
|
||||||
|
PasswordEncoder encoder = new Pbkdf2Password4jPasswordEncoder();
|
||||||
|
String result = encoder.encode("myPassword");
|
||||||
|
assertThat(encoder.matches("myPassword", result)).isTrue();
|
||||||
|
// end::default-params[]
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void customParameters() {
|
||||||
|
// tag::custom-params[]
|
||||||
|
PBKDF2Function pbkdf2Fn = PBKDF2Function.getInstance(Hmac.SHA256, 100000, 256);
|
||||||
|
PasswordEncoder encoder = new Pbkdf2Password4jPasswordEncoder(pbkdf2Fn);
|
||||||
|
String result = encoder.encode("myPassword");
|
||||||
|
assertThat(encoder.matches("myPassword", result)).isTrue();
|
||||||
|
// end::custom-params[]
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
* 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.docs.features.authentication.password4jscrypt;
|
||||||
|
|
||||||
|
import com.password4j.ScryptFunction;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.security.crypto.password4j.ScryptPassword4jPasswordEncoder;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Rob Winch
|
||||||
|
*/
|
||||||
|
public class ScryptUsageTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void defaultParams() {
|
||||||
|
// tag::default-params[]
|
||||||
|
PasswordEncoder encoder = new ScryptPassword4jPasswordEncoder();
|
||||||
|
String result = encoder.encode("myPassword");
|
||||||
|
assertThat(encoder.matches("myPassword", result)).isTrue();
|
||||||
|
// end::default-params[]
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void customParameters() {
|
||||||
|
// tag::custom-params[]
|
||||||
|
ScryptFunction scryptFn = ScryptFunction.getInstance(32768, 8, 1, 32);
|
||||||
|
PasswordEncoder encoder = new ScryptPassword4jPasswordEncoder(scryptFn);
|
||||||
|
String result = encoder.encode("myPassword");
|
||||||
|
assertThat(encoder.matches("myPassword", result)).isTrue();
|
||||||
|
// end::custom-params[]
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
* 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.kt.docs.features.authentication.password4jargon2
|
||||||
|
|
||||||
|
import com.password4j.Argon2Function
|
||||||
|
import com.password4j.types.Argon2
|
||||||
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder
|
||||||
|
import org.springframework.security.crypto.password4j.Argon2Password4jPasswordEncoder
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Rob Winch
|
||||||
|
*/
|
||||||
|
class Argon2UsageTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun defaultParams() {
|
||||||
|
// tag::default-params[]
|
||||||
|
val encoder: PasswordEncoder = Argon2Password4jPasswordEncoder()
|
||||||
|
val result = encoder.encode("myPassword")
|
||||||
|
assertThat(encoder.matches("myPassword", result)).isTrue()
|
||||||
|
// end::default-params[]
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun customParameters() {
|
||||||
|
// tag::custom-params[]
|
||||||
|
val argon2Fn = Argon2Function.getInstance(
|
||||||
|
65536, 3, 4, 32,
|
||||||
|
Argon2.ID
|
||||||
|
)
|
||||||
|
val encoder: PasswordEncoder = Argon2Password4jPasswordEncoder(argon2Fn)
|
||||||
|
val result = encoder.encode("myPassword")
|
||||||
|
assertThat(encoder.matches("myPassword", result)).isTrue()
|
||||||
|
// end::custom-params[]
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* 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.kt.docs.features.authentication.password4jballooning
|
||||||
|
|
||||||
|
import com.password4j.BalloonHashingFunction
|
||||||
|
import org.assertj.core.api.Assertions
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder
|
||||||
|
import org.springframework.security.crypto.password4j.BalloonHashingPassword4jPasswordEncoder
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Rob Winch
|
||||||
|
*/
|
||||||
|
class BallooningHashingUsageTests {
|
||||||
|
@Test
|
||||||
|
fun defaultParams() {
|
||||||
|
// tag::default-params[]
|
||||||
|
val encoder: PasswordEncoder = BalloonHashingPassword4jPasswordEncoder()
|
||||||
|
val result = encoder.encode("myPassword")
|
||||||
|
Assertions.assertThat(encoder.matches("myPassword", result)).isTrue()
|
||||||
|
// end::default-params[]
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun customParameters() {
|
||||||
|
// tag::custom-params[]
|
||||||
|
val ballooningHashingFn =
|
||||||
|
BalloonHashingFunction.getInstance("SHA-256", 1024, 3, 4, 3)
|
||||||
|
val encoder: PasswordEncoder = BalloonHashingPassword4jPasswordEncoder(ballooningHashingFn)
|
||||||
|
val result = encoder.encode("myPassword")
|
||||||
|
Assertions.assertThat(encoder.matches("myPassword", result)).isTrue()
|
||||||
|
// end::custom-params[]
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
package org.springframework.security.kt.docs.features.authentication.password4jbcrypt
|
||||||
|
|
||||||
|
import com.password4j.BcryptFunction
|
||||||
|
import org.assertj.core.api.Assertions
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder
|
||||||
|
import org.springframework.security.crypto.password4j.BcryptPassword4jPasswordEncoder
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Rob Winch
|
||||||
|
*/
|
||||||
|
class BcryptUsageTests {
|
||||||
|
@Test
|
||||||
|
fun defaultParams() {
|
||||||
|
// tag::default-params[]
|
||||||
|
val encoder: PasswordEncoder = BCryptPasswordEncoder()
|
||||||
|
val result = encoder.encode("myPassword")
|
||||||
|
Assertions.assertThat(encoder.matches("myPassword", result)).isTrue()
|
||||||
|
// end::default-params[]
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun customParameters() {
|
||||||
|
// tag::custom-params[]
|
||||||
|
val bcryptFunction = BcryptFunction.getInstance(12)
|
||||||
|
val encoder: PasswordEncoder = BcryptPassword4jPasswordEncoder(bcryptFunction)
|
||||||
|
val result = encoder.encode("myPassword")
|
||||||
|
Assertions.assertThat(encoder.matches("myPassword", result)).isTrue()
|
||||||
|
// end::custom-params[]
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
package org.springframework.security.kt.docs.features.authentication.password4jpbkdf2
|
||||||
|
|
||||||
|
import com.password4j.PBKDF2Function
|
||||||
|
import com.password4j.types.Hmac
|
||||||
|
import org.assertj.core.api.Assertions
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder
|
||||||
|
import org.springframework.security.crypto.password4j.Pbkdf2Password4jPasswordEncoder
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Rob Winch
|
||||||
|
*/
|
||||||
|
class Pbkdf2UsageTests {
|
||||||
|
@Test
|
||||||
|
fun defaultParams() {
|
||||||
|
// tag::default-params[]
|
||||||
|
val encoder: PasswordEncoder = Pbkdf2Password4jPasswordEncoder()
|
||||||
|
val result = encoder.encode("myPassword")
|
||||||
|
Assertions.assertThat(encoder.matches("myPassword", result)).isTrue()
|
||||||
|
// end::default-params[]
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun customParameters() {
|
||||||
|
// tag::custom-params[]
|
||||||
|
val pbkdf2Fn = PBKDF2Function.getInstance(Hmac.SHA256, 100000, 256)
|
||||||
|
val encoder: PasswordEncoder = Pbkdf2Password4jPasswordEncoder(pbkdf2Fn)
|
||||||
|
val result = encoder.encode("myPassword")
|
||||||
|
Assertions.assertThat(encoder.matches("myPassword", result)).isTrue()
|
||||||
|
// end::custom-params[]
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
package org.springframework.security.kt.docs.features.authentication.password4jscrypt
|
||||||
|
|
||||||
|
import com.password4j.ScryptFunction
|
||||||
|
import org.assertj.core.api.Assertions
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder
|
||||||
|
import org.springframework.security.crypto.password4j.ScryptPassword4jPasswordEncoder
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Rob Winch
|
||||||
|
*/
|
||||||
|
class ScryptUsageTests {
|
||||||
|
@Test
|
||||||
|
fun defaultParams() {
|
||||||
|
// tag::default-params[]
|
||||||
|
val encoder: PasswordEncoder = ScryptPassword4jPasswordEncoder()
|
||||||
|
val result = encoder.encode("myPassword")
|
||||||
|
Assertions.assertThat(encoder.matches("myPassword", result)).isTrue()
|
||||||
|
// end::default-params[]
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun customParameters() {
|
||||||
|
// tag::custom-params[]
|
||||||
|
val scryptFn = ScryptFunction.getInstance(32768, 8, 1, 32)
|
||||||
|
val encoder: PasswordEncoder = ScryptPassword4jPasswordEncoder(scryptFn)
|
||||||
|
val result = encoder.encode("myPassword")
|
||||||
|
Assertions.assertThat(encoder.matches("myPassword", result)).isTrue()
|
||||||
|
// end::custom-params[]
|
||||||
|
}
|
||||||
|
}
|
@ -13,6 +13,7 @@ org-jetbrains-kotlinx = "1.10.2"
|
|||||||
org-mockito = "5.17.0"
|
org-mockito = "5.17.0"
|
||||||
org-opensaml5 = "5.1.6"
|
org-opensaml5 = "5.1.6"
|
||||||
org-springframework = "7.0.0-M9"
|
org-springframework = "7.0.0-M9"
|
||||||
|
com-password4j = "1.8.2"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
ch-qos-logback-logback-classic = "ch.qos.logback:logback-classic:1.5.18"
|
ch-qos-logback-logback-classic = "ch.qos.logback:logback-classic:1.5.18"
|
||||||
@ -101,6 +102,7 @@ org-instancio-instancio-junit = "org.instancio:instancio-junit:3.7.1"
|
|||||||
|
|
||||||
spring-nullability = 'io.spring.nullability:io.spring.nullability.gradle.plugin:0.0.4'
|
spring-nullability = 'io.spring.nullability:io.spring.nullability.gradle.plugin:0.0.4'
|
||||||
webauthn4j-core = 'com.webauthn4j:webauthn4j-core:0.29.6.RELEASE'
|
webauthn4j-core = 'com.webauthn4j:webauthn4j-core:0.29.6.RELEASE'
|
||||||
|
com-password4j-password4j = { module = "com.password4j:password4j", version.ref = "com-password4j" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user