• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Avec112 / commons-security / 18761723708

23 Oct 2025 08:54PM UTC coverage: 90.018% (+0.2%) from 89.844%
18761723708

push

github

Avec112
Add password upgrade support

- Introduced `needsUpgrade` and `upgradePassword` methods in `PasswordEncoderUtils` and `CryptoUtils` for gradual algorithm migration.
- Updated README with password upgrade examples and migration tips.
- Added extensive unit tests for password upgrade logic and validation.

37 of 50 branches covered (74.0%)

Branch coverage included in aggregate %.

450 of 491 relevant lines covered (91.65%)

3.62 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.83
src/main/java/com/github/avec112/security/crypto/password/PasswordEncoderUtils.java
1
package com.github.avec112.security.crypto.password;
2

3
import org.apache.commons.lang3.Validate;
4
import org.springframework.security.crypto.argon2.Argon2PasswordEncoder;
5
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
6
import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
7
import org.springframework.security.crypto.password.PasswordEncoder;
8
import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder;
9
import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder;
10

11
import java.util.Collections;
12
import java.util.HashMap;
13
import java.util.Map;
14
import java.util.Objects;
15
import java.util.regex.Matcher;
16
import java.util.regex.Pattern;
17

18
/**
19
 * Utility class for handling password encoding and matching using various encoding schemes.
20
 * Supports common encoding types such as Argon2, BCrypt, SCrypt, and PBKDF2.
21
 * This class provides methods for encoding plaintext passwords and verifying encoded passwords.
22
 *
23
 * The encoded password can optionally include a prefix indicating the encoding type.
24
 * For example, "{argon2}$argon2id$v=..." indicates the use of Argon2 encoding.
25
 *
26
 * This class relies on the DelegatingPasswordEncoder to delegate encoding and matching
27
 * operations to the appropriate password encoder based on the specified or implied encoding type.
28
 */
29
public class PasswordEncoderUtils {
30

31
    private static final Pattern PREFIX_PATTERN = Pattern.compile("^\\{([a-zA-Z0-9_-]+)}");
3✔
32
    private static final Map<String, PasswordEncoder> ENCODERS;
33
    private static final PasswordEncoderType DEFAULT_ENCODER = PasswordEncoderType.ARGON2;
2✔
34

35
    static {
36
        Map<String, PasswordEncoder> map = new HashMap<>();
4✔
37
        map.put(PasswordEncoderType.ARGON2.id(), Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8());
6✔
38
        map.put(PasswordEncoderType.SCRYPT.id(), SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8());
6✔
39
        map.put(PasswordEncoderType.BCRYPT.id(), new BCryptPasswordEncoder());
8✔
40
        map.put(PasswordEncoderType.PBKDF2.id(), Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8());
6✔
41
        ENCODERS = Collections.unmodifiableMap(map);
3✔
42
    }
1✔
43

44
    /**
45
     * Private constructor to prevent instantiation of the {@code PasswordEncoderUtils} utility class.
46
     *
47
     * This class is designed to provide static utility methods related to password encoding
48
     * and matching, and should not be instantiated.
49
     */
50
    private PasswordEncoderUtils(){}
51

52
    /**
53
     * Creates a delegating password encoder based on the specified password encoder type.
54
     *
55
     * @param type the password encoder type to delegate to; must not be null
56
     * @return a {@link PasswordEncoder} instance configured to delegate to the specified type
57
     */
58
    private static PasswordEncoder delegating(PasswordEncoderType type) {
59
        return new DelegatingPasswordEncoder(type.id(), ENCODERS);
7✔
60
    }
61

62
    /**
63
     * Will encode with ARGON2 encoder
64
     * @param password plaintext password to encode
65
     * @return encoded password
66
     */
67
    public static String encode(String password) {
68
        Validate.notBlank(password);
3✔
69
        return encode(password, PasswordEncoderType.ARGON2);
4✔
70
    }
71

72
    /**
73
     * Encodes the provided plaintext password using the specified password encoder type.
74
     *
75
     * @param password the plaintext password to encode; must not be blank
76
     * @param encoderType the type of password encoder to use for encoding; must not be null
77
     * @return the encoded password as a string
78
     */
79
    public static String encode(String password, PasswordEncoderType encoderType) {
80
        Validate.notBlank(password);
3✔
81
        Objects.requireNonNull(encoderType);
3✔
82

83
        PasswordEncoder passwordEncoder = delegating(encoderType);
3✔
84
        return passwordEncoder.encode(password);
4✔
85
    }
86

87
    /**
88
     * Match password with ARGON2 encoded password
89
     * @param password plaintext password to encode
90
     * @param encodedPassword expected to be encoded with ARGON2
91
     * @return true if password match
92
     */
93
    public static boolean matches(String password, String encodedPassword) {
94
        Validate.notBlank(password);
3✔
95
        Validate.notBlank(encodedPassword);
3✔
96

97

98
        return matches(password, encodedPassword, DEFAULT_ENCODER);
5✔
99
    }
100

101

102
    /**
103
     * Verifies whether a plaintext password matches an encoded password using a specified password encoder type.
104
     *
105
     * @param password the plaintext password to verify; must not be blank
106
     * @param encodedPassword the encoded password to compare against; must not be blank
107
     * @param encoderType the type of password encoder to use for matching; must not be null
108
     * @return true if the plaintext password matches the encoded password, false otherwise
109
     */
110
    public static boolean matches(String password, String encodedPassword, PasswordEncoderType encoderType) {
111
        Validate.notBlank(password);
3✔
112
        Validate.notBlank(encodedPassword);
3✔
113
        Objects.requireNonNull(encoderType);
3✔
114

115
        PasswordEncoder passwordEncoder = delegating(encoderType);
3✔
116
        return passwordEncoder.matches(password, encodedPassword);
5✔
117
    }
118

119
    /**
120
     * Extracts the password encoder type (e.g. "argon2", "bcrypt", "scrypt", "pbkdf2")
121
     * from an encoded password string.
122
     *
123
     * @param encodedPassword the encoded password string, must start with {id}
124
     * @return the PasswordEncoderType if recognized
125
     * @throws IllegalArgumentException if no valid prefix is found or unsupported type
126
     */
127
    public static PasswordEncoderType getPasswordEncoderType(String encodedPassword) {
128
        Validate.notBlank(encodedPassword, "Encoded password cannot be null or blank");
6✔
129

130
        Matcher matcher = PREFIX_PATTERN.matcher(encodedPassword);
4✔
131
        if (matcher.find()) {
3!
132
            String id = matcher.group(1).toLowerCase();
5✔
133
            for (PasswordEncoderType type : PasswordEncoderType.values()) {
16!
134
                if (type.id().equalsIgnoreCase(id)) {
5✔
135
                    return type;
2✔
136
                }
137
            }
138
            throw new IllegalArgumentException("Unsupported password encoder type: " + id);
×
139
        }
140

141
        throw new IllegalArgumentException("Encoded password does not contain a valid prefix: " + encodedPassword);
×
142
    }
143

144
    /**
145
     * Convenience method returning the encoder type as string.
146
     * @return the password encoder type as string
147
     */
148
    public static String getPasswordEncoderTypeAsString(String encodedPassword) {
149
        return getPasswordEncoderType(encodedPassword).id();
4✔
150
    }
151

152
    /**
153
     * Checks if an encoded password needs to be upgraded to a stronger algorithm.
154
     * Returns true if the current encoding type is different from the target type.
155
     *
156
     * @param encodedPassword the currently encoded password
157
     * @param targetType the desired password encoder type (typically ARGON2)
158
     * @return true if the password should be re-encoded with the target type
159
     */
160
    public static boolean needsUpgrade(String encodedPassword, PasswordEncoderType targetType) {
161
        Validate.notBlank(encodedPassword);
3✔
162
        Objects.requireNonNull(targetType);
3✔
163

164
        try {
165
            PasswordEncoderType currentType = getPasswordEncoderType(encodedPassword);
3✔
166
            return currentType != targetType;
7✔
167
        } catch (IllegalArgumentException e) {
×
168
            // If we can't determine the type, assume it needs upgrade
169
            return true;
×
170
        }
171
    }
172

173
    /**
174
     * Checks if an encoded password needs to be upgraded to the default algorithm (ARGON2).
175
     *
176
     * @param encodedPassword the currently encoded password
177
     * @return true if the password should be re-encoded with ARGON2
178
     */
179
    public static boolean needsUpgrade(String encodedPassword) {
180
        return needsUpgrade(encodedPassword, DEFAULT_ENCODER);
4✔
181
    }
182

183
    /**
184
     * Upgrades an encoded password from one encoder type to another.
185
     * This method verifies the raw password against the old encoded password,
186
     * and if valid, re-encodes it with the target encoder type.
187
     *
188
     * @param rawPassword the plaintext password to verify and re-encode
189
     * @param oldEncodedPassword the currently encoded password
190
     * @param targetType the desired password encoder type for the upgrade
191
     * @return the newly encoded password with the target encoder type
192
     * @throws IllegalArgumentException if the raw password does not match the old encoded password
193
     */
194
    public static String upgradePassword(String rawPassword, String oldEncodedPassword, PasswordEncoderType targetType) {
195
        Validate.notBlank(rawPassword);
3✔
196
        Validate.notBlank(oldEncodedPassword);
3✔
197
        Objects.requireNonNull(targetType);
3✔
198

199
        // Get the current encoder type
200
        PasswordEncoderType currentType = getPasswordEncoderType(oldEncodedPassword);
3✔
201

202
        // Verify the raw password matches the old encoded password
203
        if (!matches(rawPassword, oldEncodedPassword, currentType)) {
5✔
204
            throw new IllegalArgumentException("Raw password does not match the encoded password");
5✔
205
        }
206

207
        // Re-encode with the target encoder type
208
        return encode(rawPassword, targetType);
4✔
209
    }
210

211
    /**
212
     * Upgrades an encoded password to the default encoder type (ARGON2).
213
     * This method verifies the raw password against the old encoded password,
214
     * and if valid, re-encodes it with ARGON2.
215
     *
216
     * @param rawPassword the plaintext password to verify and re-encode
217
     * @param oldEncodedPassword the currently encoded password
218
     * @return the newly encoded password with ARGON2
219
     * @throws IllegalArgumentException if the raw password does not match the old encoded password
220
     */
221
    public static String upgradePassword(String rawPassword, String oldEncodedPassword) {
222
        return upgradePassword(rawPassword, oldEncodedPassword, DEFAULT_ENCODER);
5✔
223
    }
224

225
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc