Skip to content

Commit 441b4cd

Browse files
Create a specific implementation for BalloonHashing and PBKDF2 password encoders using Password4j library
Closes gh-17706
1 parent 564f630 commit 441b4cd

File tree

5 files changed

+693
-3
lines changed

5 files changed

+693
-3
lines changed
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: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
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.Hash;
24+
import com.password4j.PBKDF2Function;
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 PBKDF2 hashing algorithm.
33+
*
34+
* <p>
35+
* PBKDF2 is a key derivation function designed to be computationally expensive to thwart
36+
* dictionary and brute force attacks. This implementation handles the salt management
37+
* explicitly since Password4j's PBKDF2 implementation does not include the salt in the
38+
* 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 PBKDF2 settings (recommended)
55+
* PasswordEncoder encoder = new Pbkdf2Password4jPasswordEncoder();
56+
*
57+
* // Using custom PBKDF2 function
58+
* PasswordEncoder customEncoder = new Pbkdf2Password4jPasswordEncoder(
59+
* PBKDF2Function.getInstance(Algorithm.HMAC_SHA256, 100000, 256));
60+
* }</pre>
61+
*
62+
* @author Mehrdad Bozorgmehr
63+
* @since 7.0
64+
* @see PBKDF2Function
65+
* @see AlgorithmFinder#getPBKDF2Instance()
66+
*/
67+
public class Pbkdf2Password4jPasswordEncoder extends AbstractValidatingPasswordEncoder {
68+
69+
private static final String DELIMITER = ":";
70+
71+
private static final int DEFAULT_SALT_LENGTH = 32;
72+
73+
private final PBKDF2Function pbkdf2Function;
74+
75+
private final SecureRandom secureRandom;
76+
77+
private final int saltLength;
78+
79+
/**
80+
* Constructs a PBKDF2 password encoder using the default PBKDF2 configuration from
81+
* Password4j's AlgorithmFinder.
82+
*/
83+
public Pbkdf2Password4jPasswordEncoder() {
84+
this(AlgorithmFinder.getPBKDF2Instance());
85+
}
86+
87+
/**
88+
* Constructs a PBKDF2 password encoder with a custom PBKDF2 function.
89+
* @param pbkdf2Function the PBKDF2 function to use for encoding passwords, must not
90+
* be null
91+
* @throws IllegalArgumentException if pbkdf2Function is null
92+
*/
93+
public Pbkdf2Password4jPasswordEncoder(PBKDF2Function pbkdf2Function) {
94+
this(pbkdf2Function, DEFAULT_SALT_LENGTH);
95+
}
96+
97+
/**
98+
* Constructs a PBKDF2 password encoder with a custom PBKDF2 function and salt length.
99+
* @param pbkdf2Function the PBKDF2 function to use for encoding passwords, must not
100+
* be null
101+
* @param saltLength the length of the salt in bytes, must be positive
102+
* @throws IllegalArgumentException if pbkdf2Function is null or saltLength is not
103+
* positive
104+
*/
105+
public Pbkdf2Password4jPasswordEncoder(PBKDF2Function pbkdf2Function, int saltLength) {
106+
Assert.notNull(pbkdf2Function, "pbkdf2Function cannot be null");
107+
Assert.isTrue(saltLength > 0, "saltLength must be positive");
108+
this.pbkdf2Function = pbkdf2Function;
109+
this.saltLength = saltLength;
110+
this.secureRandom = new SecureRandom();
111+
}
112+
113+
@Override
114+
protected String encodeNonNullPassword(String rawPassword) {
115+
byte[] salt = new byte[this.saltLength];
116+
this.secureRandom.nextBytes(salt);
117+
118+
Hash hash = Password.hash(rawPassword).addSalt(salt).with(this.pbkdf2Function);
119+
String encodedSalt = Base64.getEncoder().encodeToString(salt);
120+
String encodedHash = hash.getResult();
121+
122+
return encodedSalt + DELIMITER + encodedHash;
123+
}
124+
125+
@Override
126+
protected boolean matchesNonNull(String rawPassword, String encodedPassword) {
127+
if (!encodedPassword.contains(DELIMITER)) {
128+
return false;
129+
}
130+
131+
String[] parts = encodedPassword.split(DELIMITER, 2);
132+
if (parts.length != 2) {
133+
return false;
134+
}
135+
136+
try {
137+
byte[] salt = Base64.getDecoder().decode(parts[0]);
138+
String expectedHash = parts[1];
139+
140+
Hash hash = Password.hash(rawPassword).addSalt(salt).with(this.pbkdf2Function);
141+
return expectedHash.equals(hash.getResult());
142+
}
143+
catch (IllegalArgumentException ex) {
144+
// Invalid Base64 encoding
145+
return false;
146+
}
147+
}
148+
149+
@Override
150+
protected boolean upgradeEncodingNonNull(String encodedPassword) {
151+
// For now, we'll return false to maintain existing behavior
152+
// This could be enhanced in the future to check if the encoding parameters
153+
// match the current configuration
154+
return false;
155+
}
156+
157+
}

0 commit comments

Comments
 (0)