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

  1. Use a valid full salt string: $2a$NN$ where NN is a two-digit cost 04-31 followed by '$'
  2. Generate the salt via BCrypt.gensalt(cost) instead of writing it by hand
  3. 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

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.