Skip to content

Commit d9494ca

Browse files
Add password4j implementation of PasswordEncoder
Closes gh-17706 Signed-off-by: Mehrdad <[email protected]>
1 parent 49f308a commit d9494ca

File tree

7 files changed

+883
-1
lines changed

7 files changed

+883
-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 'com.password4j:password4j'
1112

1213
testImplementation "org.assertj:assertj-core"
1314
testImplementation "org.junit.jupiter:junit-jupiter-api"

crypto/src/main/java/org/springframework/security/crypto/factory/PasswordEncoderFactories.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
2525
import org.springframework.security.crypto.password.PasswordEncoder;
2626
import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder;
27+
import org.springframework.security.crypto.password4j.Password4jPasswordEncoder;
2728
import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder;
2829

2930
/**
@@ -65,6 +66,10 @@ private PasswordEncoderFactories() {
6566
* <li>argon2 - {@link Argon2PasswordEncoder#defaultsForSpringSecurity_v5_2()}</li>
6667
* <li>argon2@SpringSecurity_v5_8 -
6768
* {@link Argon2PasswordEncoder#defaultsForSpringSecurity_v5_8()}</li>
69+
* <li>password4j-bcrypt - {@link Password4jPasswordEncoder} with BCrypt</li>
70+
* <li>password4j-scrypt - {@link Password4jPasswordEncoder} with SCrypt</li>
71+
* <li>password4j-argon2 - {@link Password4jPasswordEncoder} with Argon2</li>
72+
* <li>password4j-pbkdf2 - {@link Password4jPasswordEncoder} with PBKDF2</li>
6873
* </ul>
6974
* @return the {@link PasswordEncoder} to use
7075
*/
@@ -87,6 +92,14 @@ public static PasswordEncoder createDelegatingPasswordEncoder() {
8792
encoders.put("sha256", new org.springframework.security.crypto.password.StandardPasswordEncoder());
8893
encoders.put("argon2", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_2());
8994
encoders.put("argon2@SpringSecurity_v5_8", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8());
95+
96+
// Password4j implementations
97+
encoders.put("password4j-bcrypt", Password4jPasswordEncoder.bcrypt(10));
98+
encoders.put("password4j-scrypt", Password4jPasswordEncoder.scrypt(16384, 8, 1, 32));
99+
encoders.put("password4j-argon2", Password4jPasswordEncoder.argon2(65536, 3, 4, 32,
100+
com.password4j.types.Argon2.ID));
101+
encoders.put("password4j-pbkdf2", Password4jPasswordEncoder.pbkdf2(310000, 32));
102+
90103
return new DelegatingPasswordEncoder(encodingId, encoders);
91104
}
92105

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
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.*;
20+
import com.password4j.types.Argon2;
21+
import org.apache.commons.logging.Log;
22+
import org.apache.commons.logging.LogFactory;
23+
import org.springframework.security.crypto.password.AbstractValidatingPasswordEncoder;
24+
import org.springframework.util.Assert;
25+
26+
/**
27+
* Implementation of {@link org.springframework.security.crypto.password.PasswordEncoder} that uses the Password4j library.
28+
* This encoder supports multiple password hashing algorithms including BCrypt, SCrypt, Argon2, and PBKDF2.
29+
*
30+
* <p>The encoder determines the algorithm used based on the algorithm type specified during construction.
31+
* For verification, it can automatically detect the algorithm used in existing hashes.</p>
32+
*
33+
* <p>This implementation is thread-safe and can be shared across multiple threads.</p>
34+
*
35+
* @author Mehrdad Bozorgmehr
36+
* @since 6.5
37+
*/
38+
public class Password4jPasswordEncoder extends AbstractValidatingPasswordEncoder {
39+
40+
private final Log logger = LogFactory.getLog(getClass());
41+
42+
private final HashingFunction hashingFunction;
43+
44+
private final Password4jAlgorithm algorithm;
45+
46+
47+
/**
48+
* Enumeration of supported Password4j algorithms.
49+
*/
50+
public enum Password4jAlgorithm {
51+
/**
52+
* BCrypt algorithm.
53+
*/
54+
BCRYPT,
55+
/**
56+
* SCrypt algorithm.
57+
*/
58+
SCRYPT,
59+
/**
60+
* Argon2 algorithm.
61+
*/
62+
ARGON2,
63+
/**
64+
* PBKDF2 algorithm.
65+
*/
66+
PBKDF2,
67+
/**
68+
* Compressed PBKDF2 algorithm.
69+
*/
70+
COMPRESSED_PBKDF2
71+
}
72+
73+
/**
74+
* Constructs a Password4j password encoder with the default BCrypt algorithm.
75+
*/
76+
public Password4jPasswordEncoder() {
77+
this(Password4jAlgorithm.BCRYPT);
78+
}
79+
80+
/**
81+
* Constructs a Password4j password encoder with the specified algorithm using default parameters.
82+
*
83+
* @param algorithm the password hashing algorithm to use
84+
*/
85+
public Password4jPasswordEncoder(Password4jAlgorithm algorithm) {
86+
Assert.notNull(algorithm, "algorithm cannot be null");
87+
this.algorithm = algorithm;
88+
this.hashingFunction = createDefaultHashingFunction(algorithm);
89+
}
90+
91+
/**
92+
* Constructs a Password4j password encoder with a custom hashing function.
93+
*
94+
* @param hashingFunction the custom hashing function to use
95+
* @param algorithm the password hashing algorithm type
96+
*/
97+
public Password4jPasswordEncoder(HashingFunction hashingFunction, Password4jAlgorithm algorithm) {
98+
Assert.notNull(hashingFunction, "hashingFunction cannot be null");
99+
Assert.notNull(algorithm, "algorithm cannot be null");
100+
this.hashingFunction = hashingFunction;
101+
this.algorithm = algorithm;
102+
}
103+
104+
/**
105+
* Creates a Password4j password encoder with BCrypt algorithm and specified rounds.
106+
*
107+
* @param rounds the number of rounds (cost factor) for BCrypt
108+
* @return a new Password4j password encoder
109+
*/
110+
public static Password4jPasswordEncoder bcrypt(int rounds) {
111+
return new Password4jPasswordEncoder(BcryptFunction.getInstance(rounds), Password4jAlgorithm.BCRYPT);
112+
}
113+
114+
/**
115+
* Creates a Password4j password encoder with SCrypt algorithm and specified parameters.
116+
*
117+
* @param workFactor the work factor (N parameter)
118+
* @param resources the resources (r parameter)
119+
* @param parallelization the parallelization (p parameter)
120+
* @param derivedKeyLength the derived key length
121+
* @return a new Password4j password encoder
122+
*/
123+
public static Password4jPasswordEncoder scrypt(int workFactor, int resources, int parallelization, int derivedKeyLength) {
124+
return new Password4jPasswordEncoder(
125+
ScryptFunction.getInstance(workFactor, resources, parallelization, derivedKeyLength),
126+
Password4jAlgorithm.SCRYPT
127+
);
128+
}
129+
130+
/**
131+
* Creates a Password4j password encoder with Argon2 algorithm and specified parameters.
132+
*
133+
* @param memory the memory cost
134+
* @param iterations the number of iterations
135+
* @param parallelism the parallelism
136+
* @param outputLength the output length
137+
* @param type the Argon2 type
138+
* @return a new Password4j password encoder
139+
*/
140+
public static Password4jPasswordEncoder argon2(int memory, int iterations, int parallelism, int outputLength, Argon2 type) {
141+
return new Password4jPasswordEncoder(
142+
Argon2Function.getInstance(memory, iterations, parallelism, outputLength, type),
143+
Password4jAlgorithm.ARGON2
144+
);
145+
}
146+
147+
/**
148+
* Creates a Password4j password encoder with PBKDF2 algorithm and specified parameters.
149+
*
150+
* @param iterations the number of iterations
151+
* @param derivedKeyLength the derived key length
152+
* @return a new Password4j password encoder
153+
*/
154+
public static Password4jPasswordEncoder pbkdf2(int iterations, int derivedKeyLength) {
155+
return new Password4jPasswordEncoder(
156+
CompressedPBKDF2Function.getInstance("SHA256", iterations, derivedKeyLength),
157+
Password4jAlgorithm.PBKDF2
158+
);
159+
}
160+
161+
/**
162+
* Creates a Password4j password encoder with compressed PBKDF2 algorithm.
163+
*
164+
* @param iterations the number of iterations
165+
* @param derivedKeyLength the derived key length
166+
* @return a new Password4j password encoder
167+
*/
168+
public static Password4jPasswordEncoder compressedPbkdf2(int iterations, int derivedKeyLength) {
169+
return new Password4jPasswordEncoder(
170+
CompressedPBKDF2Function.getInstance("SHA256", iterations, derivedKeyLength),
171+
Password4jAlgorithm.COMPRESSED_PBKDF2
172+
);
173+
}
174+
175+
/**
176+
* Creates a Password4j password encoder with default settings for Spring Security v5.8+.
177+
* This uses BCrypt with 10 rounds.
178+
*
179+
* @return a new Password4j password encoder with recommended defaults
180+
* @since 6.5
181+
*/
182+
public static Password4jPasswordEncoder defaultsForSpringSecurity() {
183+
return bcrypt(10);
184+
}
185+
186+
@Override
187+
protected String encodeNonNullPassword(String rawPassword) {
188+
try {
189+
Hash hash = Password.hash(rawPassword).with(this.hashingFunction);
190+
return hash.getResult();
191+
} catch (Exception ex) {
192+
throw new IllegalStateException("Failed to encode password using Password4j", ex);
193+
}
194+
}
195+
196+
@Override
197+
protected boolean matchesNonNull(String rawPassword, String encodedPassword) {
198+
try {
199+
// Use the specific hashing function for verification
200+
return Password.check(rawPassword, encodedPassword).with(this.hashingFunction);
201+
} catch (Exception ex) {
202+
this.logger.warn("Password verification failed for encoded password: " + encodedPassword, ex);
203+
return false;
204+
}
205+
}
206+
207+
@Override
208+
protected boolean upgradeEncodingNonNull(String encodedPassword) {
209+
// Password4j handles upgrade detection internally for most algorithms
210+
// For now, we'll return false to maintain existing behavior
211+
return false;
212+
}
213+
214+
/**
215+
* Creates a default hashing function for the specified algorithm.
216+
*
217+
* @param algorithm the password hashing algorithm
218+
* @return the default hashing function
219+
*/
220+
private static HashingFunction createDefaultHashingFunction(Password4jAlgorithm algorithm) {
221+
return switch (algorithm) {
222+
case BCRYPT -> BcryptFunction.getInstance(10); // Default 10 rounds
223+
case SCRYPT -> ScryptFunction.getInstance(16384, 8, 1, 32); // Default parameters
224+
case ARGON2 -> Argon2Function.getInstance(65536, 3, 4, 32, Argon2.ID); // Default parameters
225+
case PBKDF2 ->
226+
CompressedPBKDF2Function.getInstance("SHA256", 310000, 32); // Use compressed format for self-contained encoding
227+
case COMPRESSED_PBKDF2 -> CompressedPBKDF2Function.getInstance("SHA256", 310000, 32);
228+
};
229+
}
230+
231+
/**
232+
* Gets the algorithm used by this encoder.
233+
*
234+
* @return the password hashing algorithm
235+
*/
236+
public Password4jAlgorithm getAlgorithm() {
237+
return this.algorithm;
238+
}
239+
240+
/**
241+
* Gets the hashing function used by this encoder.
242+
*
243+
* @return the hashing function
244+
*/
245+
public HashingFunction getHashingFunction() {
246+
return this.hashingFunction;
247+
}
248+
249+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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+
18+
@NullMarked
19+
package org.springframework.security.crypto.password4j;
20+
21+
import org.jspecify.annotations.NullMarked;

0 commit comments

Comments
 (0)