Skip to content

Commit 5ca18a3

Browse files
authored
Add password4j implementation of PasswordEncoder
2 parents 68427b1 + d0372ef commit 5ca18a3

30 files changed

+2515
-1
lines changed

crypto/spring-security-crypto.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ dependencies {
88
management platform(project(":spring-security-dependencies"))
99
optional 'org.springframework:spring-core'
1010
optional 'org.bouncycastle:bcpkix-jdk18on'
11+
optional libs.com.password4j.password4j
1112

1213
testImplementation "org.assertj:assertj-core"
1314
testImplementation "org.junit.jupiter:junit-jupiter-api"
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Copyright 2004-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.crypto.password4j;
18+
19+
import com.password4j.AlgorithmFinder;
20+
import com.password4j.Argon2Function;
21+
22+
/**
23+
* Implementation of {@link org.springframework.security.crypto.password.PasswordEncoder}
24+
* that uses the Password4j library with Argon2 hashing algorithm.
25+
*
26+
* <p>
27+
* Argon2 is the winner of the Password Hashing Competition (2015) and is recommended for
28+
* new applications. It provides excellent resistance against GPU-based attacks and
29+
* includes built-in salt generation. This implementation leverages Password4j's Argon2
30+
* support which properly includes the salt in the output hash.
31+
* </p>
32+
*
33+
* <p>
34+
* This implementation is thread-safe and can be shared across multiple threads.
35+
* </p>
36+
*
37+
* <p>
38+
* <strong>Usage Examples:</strong>
39+
* </p>
40+
* <pre>{@code
41+
* // Using default Argon2 settings (recommended)
42+
* PasswordEncoder encoder = new Argon2Password4jPasswordEncoder();
43+
*
44+
* // Using custom Argon2 configuration
45+
* PasswordEncoder customEncoder = new Argon2Password4jPasswordEncoder(
46+
* Argon2Function.getInstance(65536, 3, 4, 32, Argon2.ID));
47+
* }</pre>
48+
*
49+
* @author Mehrdad Bozorgmehr
50+
* @since 7.0
51+
* @see Argon2Function
52+
* @see AlgorithmFinder#getArgon2Instance()
53+
*/
54+
public class Argon2Password4jPasswordEncoder extends Password4jPasswordEncoder {
55+
56+
/**
57+
* Constructs an Argon2 password encoder using the default Argon2 configuration from
58+
* Password4j's AlgorithmFinder.
59+
*/
60+
public Argon2Password4jPasswordEncoder() {
61+
super(AlgorithmFinder.getArgon2Instance());
62+
}
63+
64+
/**
65+
* Constructs an Argon2 password encoder with a custom Argon2 function.
66+
* @param argon2Function the Argon2 function to use for encoding passwords, must not
67+
* be null
68+
* @throws IllegalArgumentException if argon2Function is null
69+
*/
70+
public Argon2Password4jPasswordEncoder(Argon2Function argon2Function) {
71+
super(argon2Function);
72+
}
73+
74+
}
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
/*
2+
* Copyright 2004-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.crypto.password4j;
18+
19+
import java.security.SecureRandom;
20+
import java.util.Base64;
21+
22+
import com.password4j.AlgorithmFinder;
23+
import com.password4j.BalloonHashingFunction;
24+
import com.password4j.Hash;
25+
import com.password4j.Password;
26+
27+
import org.springframework.security.crypto.password.AbstractValidatingPasswordEncoder;
28+
import org.springframework.util.Assert;
29+
30+
/**
31+
* Implementation of {@link org.springframework.security.crypto.password.PasswordEncoder}
32+
* that uses the Password4j library with Balloon hashing algorithm.
33+
*
34+
* <p>
35+
* Balloon hashing is a memory-hard password hashing algorithm designed to be resistant to
36+
* both time-memory trade-off attacks and side-channel attacks. This implementation
37+
* handles the salt management explicitly since Password4j's Balloon hashing
38+
* implementation does not include the salt in the output hash.
39+
* </p>
40+
*
41+
* <p>
42+
* The encoded password format is: {salt}:{hash} where both salt and hash are Base64
43+
* encoded.
44+
* </p>
45+
*
46+
* <p>
47+
* This implementation is thread-safe and can be shared across multiple threads.
48+
* </p>
49+
*
50+
* <p>
51+
* <strong>Usage Examples:</strong>
52+
* </p>
53+
* <pre>{@code
54+
* // Using default Balloon hashing settings (recommended)
55+
* PasswordEncoder encoder = new BalloonHashingPassword4jPasswordEncoder();
56+
*
57+
* // Using custom Balloon hashing function
58+
* PasswordEncoder customEncoder = new BalloonHashingPassword4jPasswordEncoder(
59+
* BalloonHashingFunction.getInstance(1024, 3, 4, "SHA-256"));
60+
* }</pre>
61+
*
62+
* @author Mehrdad Bozorgmehr
63+
* @since 7.0
64+
* @see BalloonHashingFunction
65+
* @see AlgorithmFinder#getBalloonHashingInstance()
66+
*/
67+
public class BalloonHashingPassword4jPasswordEncoder extends AbstractValidatingPasswordEncoder {
68+
69+
private static final String DELIMITER = ":";
70+
71+
private static final int DEFAULT_SALT_LENGTH = 32;
72+
73+
private final BalloonHashingFunction balloonHashingFunction;
74+
75+
private final SecureRandom secureRandom;
76+
77+
private final int saltLength;
78+
79+
/**
80+
* Constructs a Balloon hashing password encoder using the default Balloon hashing
81+
* configuration from Password4j's AlgorithmFinder.
82+
*/
83+
public BalloonHashingPassword4jPasswordEncoder() {
84+
this(AlgorithmFinder.getBalloonHashingInstance());
85+
}
86+
87+
/**
88+
* Constructs a Balloon hashing password encoder with a custom Balloon hashing
89+
* function.
90+
* @param balloonHashingFunction the Balloon hashing function to use for encoding
91+
* passwords, must not be null
92+
* @throws IllegalArgumentException if balloonHashingFunction is null
93+
*/
94+
public BalloonHashingPassword4jPasswordEncoder(BalloonHashingFunction balloonHashingFunction) {
95+
this(balloonHashingFunction, DEFAULT_SALT_LENGTH);
96+
}
97+
98+
/**
99+
* Constructs a Balloon hashing password encoder with a custom Balloon hashing
100+
* function and salt length.
101+
* @param balloonHashingFunction the Balloon hashing function to use for encoding
102+
* passwords, must not be null
103+
* @param saltLength the length of the salt in bytes, must be positive
104+
* @throws IllegalArgumentException if balloonHashingFunction is null or saltLength is
105+
* not positive
106+
*/
107+
public BalloonHashingPassword4jPasswordEncoder(BalloonHashingFunction balloonHashingFunction, int saltLength) {
108+
Assert.notNull(balloonHashingFunction, "balloonHashingFunction cannot be null");
109+
Assert.isTrue(saltLength > 0, "saltLength must be positive");
110+
this.balloonHashingFunction = balloonHashingFunction;
111+
this.saltLength = saltLength;
112+
this.secureRandom = new SecureRandom();
113+
}
114+
115+
@Override
116+
protected String encodeNonNullPassword(String rawPassword) {
117+
byte[] salt = new byte[this.saltLength];
118+
this.secureRandom.nextBytes(salt);
119+
120+
Hash hash = Password.hash(rawPassword).addSalt(salt).with(this.balloonHashingFunction);
121+
String encodedSalt = Base64.getEncoder().encodeToString(salt);
122+
String encodedHash = hash.getResult();
123+
124+
return encodedSalt + DELIMITER + encodedHash;
125+
}
126+
127+
@Override
128+
protected boolean matchesNonNull(String rawPassword, String encodedPassword) {
129+
if (!encodedPassword.contains(DELIMITER)) {
130+
return false;
131+
}
132+
133+
String[] parts = encodedPassword.split(DELIMITER, 2);
134+
if (parts.length != 2) {
135+
return false;
136+
}
137+
138+
try {
139+
byte[] salt = Base64.getDecoder().decode(parts[0]);
140+
String expectedHash = parts[1];
141+
142+
Hash hash = Password.hash(rawPassword).addSalt(salt).with(this.balloonHashingFunction);
143+
return expectedHash.equals(hash.getResult());
144+
}
145+
catch (IllegalArgumentException ex) {
146+
// Invalid Base64 encoding
147+
return false;
148+
}
149+
}
150+
151+
@Override
152+
protected boolean upgradeEncodingNonNull(String encodedPassword) {
153+
// For now, we'll return false to maintain existing behavior
154+
// This could be enhanced in the future to check if the encoding parameters
155+
// match the current configuration
156+
return false;
157+
}
158+
159+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
* Copyright 2004-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.crypto.password4j;
18+
19+
import com.password4j.AlgorithmFinder;
20+
import com.password4j.BcryptFunction;
21+
22+
/**
23+
* Implementation of {@link org.springframework.security.crypto.password.PasswordEncoder}
24+
* that uses the Password4j library with BCrypt hashing algorithm.
25+
*
26+
* <p>
27+
* BCrypt is a well-established password hashing algorithm that includes built-in salt
28+
* generation and is resistant to rainbow table attacks. This implementation leverages
29+
* Password4j's BCrypt support which properly includes the salt in the output hash.
30+
* </p>
31+
*
32+
* <p>
33+
* This implementation is thread-safe and can be shared across multiple threads.
34+
* </p>
35+
*
36+
* <p>
37+
* <strong>Usage Examples:</strong>
38+
* </p>
39+
* <pre>{@code
40+
* // Using default BCrypt settings (recommended)
41+
* PasswordEncoder encoder = new BcryptPassword4jPasswordEncoder();
42+
*
43+
* // Using custom round count
44+
* PasswordEncoder customEncoder = new BcryptPassword4jPasswordEncoder(BcryptFunction.getInstance(12));
45+
* }</pre>
46+
*
47+
* @author Mehrdad Bozorgmehr
48+
* @since 7.0
49+
* @see BcryptFunction
50+
* @see AlgorithmFinder#getBcryptInstance()
51+
*/
52+
public class BcryptPassword4jPasswordEncoder extends Password4jPasswordEncoder {
53+
54+
/**
55+
* Constructs a BCrypt password encoder using the default BCrypt configuration from
56+
* Password4j's AlgorithmFinder.
57+
*/
58+
public BcryptPassword4jPasswordEncoder() {
59+
super(AlgorithmFinder.getBcryptInstance());
60+
}
61+
62+
/**
63+
* Constructs a BCrypt password encoder with a custom BCrypt function.
64+
* @param bcryptFunction the BCrypt function to use for encoding passwords, must not
65+
* be null
66+
* @throws IllegalArgumentException if bcryptFunction is null
67+
*/
68+
public BcryptPassword4jPasswordEncoder(BcryptFunction bcryptFunction) {
69+
super(bcryptFunction);
70+
}
71+
72+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
* Copyright 2004-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.crypto.password4j;
18+
19+
import com.password4j.Hash;
20+
import com.password4j.HashingFunction;
21+
import com.password4j.Password;
22+
23+
import org.springframework.security.crypto.password.AbstractValidatingPasswordEncoder;
24+
import org.springframework.util.Assert;
25+
26+
/**
27+
* Abstract base class for Password4j-based password encoders. This class provides the
28+
* common functionality for password encoding and verification using the Password4j
29+
* library.
30+
*
31+
* <p>
32+
* This class is package-private and should not be used directly. Instead, use the
33+
* specific public subclasses that support verified hashing algorithms such as BCrypt,
34+
* Argon2, and SCrypt implementations.
35+
* </p>
36+
*
37+
* <p>
38+
* This implementation is thread-safe and can be shared across multiple threads.
39+
* </p>
40+
*
41+
* @author Mehrdad Bozorgmehr
42+
* @since 7.0
43+
*/
44+
abstract class Password4jPasswordEncoder extends AbstractValidatingPasswordEncoder {
45+
46+
private final HashingFunction hashingFunction;
47+
48+
/**
49+
* Constructs a Password4j password encoder with the specified hashing function. This
50+
* constructor is package-private and intended for use by subclasses only.
51+
* @param hashingFunction the hashing function to use for encoding passwords, must not
52+
* be null
53+
* @throws IllegalArgumentException if hashingFunction is null
54+
*/
55+
Password4jPasswordEncoder(HashingFunction hashingFunction) {
56+
Assert.notNull(hashingFunction, "hashingFunction cannot be null");
57+
this.hashingFunction = hashingFunction;
58+
}
59+
60+
@Override
61+
protected String encodeNonNullPassword(String rawPassword) {
62+
Hash hash = Password.hash(rawPassword).with(this.hashingFunction);
63+
return hash.getResult();
64+
}
65+
66+
@Override
67+
protected boolean matchesNonNull(String rawPassword, String encodedPassword) {
68+
return Password.check(rawPassword, encodedPassword).with(this.hashingFunction);
69+
}
70+
71+
@Override
72+
protected boolean upgradeEncodingNonNull(String encodedPassword) {
73+
// Password4j handles upgrade detection internally for most algorithms
74+
// For now, we'll return false to maintain existing behavior
75+
return false;
76+
}
77+
78+
}

0 commit comments

Comments
 (0)