spring-projects/spring-security · error · IllegalArgumentException
Missing salt rounds
Error message
Missing salt rounds
What it means
hashpw() reads the two-digit cost factor immediately after the revision separator and requires the character at off+2 (which should terminate the cost field) to be <= '$'. If a digit or other character appears where the '$' separator is expected, the cost field is malformed and "Missing salt rounds" is thrown.
Source
Thrown at crypto/src/main/java/org/springframework/security/crypto/bcrypt/BCrypt.java:644
}
if (salt.charAt(0) != '$' || salt.charAt(1) != '2') {
throw new IllegalArgumentException("Invalid salt version");
}
if (salt.charAt(2) == '$') {
off = 3;
}
else {
minor = salt.charAt(2);
if ((minor != 'a' && minor != 'x' && minor != 'y' && minor != 'b') || salt.charAt(3) != '$') {
throw new IllegalArgumentException("Invalid salt revision");
}
off = 4;
}
// Extract number of rounds
if (salt.charAt(off + 2) > '$') {
throw new IllegalArgumentException("Missing salt rounds");
}
if (off == 4 && saltLength < 29) {
throw new IllegalArgumentException("Invalid salt");
}
rounds = Integer.parseInt(salt.substring(off, off + 2));
real_salt = salt.substring(off + 3, off + 25);
saltb = decode_base64(real_salt, BCRYPT_SALT_LEN);
if (minor >= 'a') {
passwordb = Arrays.copyOf(passwordb, passwordb.length + 1);
}
B = new BCrypt();
hashed = B.crypt_raw(passwordb, saltb, rounds, minor == 'x', minor == 'a' ? 0x10000 : 0, for_check);
rs.append("$2");View on GitHub (pinned to 96852e8860)
Solutions
- Use a valid full salt string: $2a$NN$ where NN is a two-digit cost 04-31 followed by '$'
- Generate the salt via BCrypt.gensalt(cost) instead of writing it by hand
- Never edit the cost portion of a stored hash; re-hash instead
Example fix
// before String salt = "$2a$100$" + rawSalt; // after String salt = BCrypt.gensalt(10); // $2a$10$<22 chars>
Defensive patterns
Strategy: validation
Validate before calling
if (!salt.matches("^\\$2[abxy]\\$\\d{2}\\$")) {
throw new IllegalArgumentException("Salt must contain a two-digit cost followed by $");
} Type guard
boolean hasTwoDigitCost(String s) {
return s.length() > 6 && Character.isDigit(s.charAt(4)) && Character.isDigit(s.charAt(5)) && s.charAt(6) == '$';
} Try / catch
try {
hash = BCrypt.hashpw(pw, salt);
} catch (IllegalArgumentException e) {
if (e.getMessage().equals("Missing salt rounds")) { /* reject malformed hash */ }
} Prevention
- Remember bcrypt cost is exactly two digits (04-31); 3-digit costs are not supported
- Regenerate hashes rather than editing the cost field in place
When it happens
Trigger: Salt strings like $2a$1x$... or $2a$123$... where the two rounds digits are not followed by '$', e.g. a three-digit cost ($2a$100$) or a corrupted middle segment.
Common situations: Hand-editing hashes to change cost; hashes from libraries supporting 3-digit costs; truncation/concatenation bugs when storing hashes; typos when hardcoding a salt in tests.
Understand the failure class
Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.
Related errors
AI-assisted analysis of spring-projects/spring-security@96852e8860 (2026-09-10).
Data as JSON: /api/errors/f7c077e1a955afbc.
Report an issue: GitHub.