spring-projects/spring-security · warning

Malformed password hash

Error message

Malformed password hash

What it means

Argon2PasswordEncoder.matchesNonNull logs this warning when Argon2EncodingUtils.decode(encodedPassword) throws IllegalArgumentException, i.e. the stored encoded password is not a valid modular-crypt-format Argon2 hash. The encoder cannot extract the salt/parameters from the string, so it returns false instead of throwing — the match simply fails.

Solutions

  1. Verify the stored hash starts with $argon2id$ (or the variant you configured) and is complete/untruncated.
  2. Check the DB column length; Argon2 hashes are ~97+ chars, so widen the column if the hash was cut off.
  3. If legacy hashes coexist, use DelegatingPasswordEncoder (PasswordEncoderFactories.createDelegatingPasswordEncoder) so each hash is decoded by the right encoder.
  4. Re-encode the affected accounts' passwords (e.g. via a password-reset flow) so they are stored in Argon2 format.

Example fix

// before
boolean ok = new Argon2PasswordEncoder(16, 32, 1, 16384, 2).matches(raw, legacyBcryptHash);

// after
PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
boolean ok = encoder.matches(raw, "{bcrypt}" + legacyBcryptHash);
Defensive patterns

Strategy: validation

Validate before calling

boolean validArgon2Hash = encoded != null && encoded.matches("\\$argon2(id|i|d)\\$v=\\d+\\$m=\\d+,t=\\d+,p=\\d+\\$[A-Za-z0-9+/]+\\$[A-Za-z0-9+/]+");
if (!validArgon2Hash) { /* treat as no-match / migrate account */ }

Try / catch

matches() never throws for malformed hashes (it returns false and logs); treat a persistent false as a signal to inspect/re-encode the stored hash rather than catching an exception.

Prevention

When it happens

Trigger: Calling passwordEncoder.matches(rawPassword, encodedPassword) where encodedPassword was not produced by Argon2PasswordEncoder.encode (e.g. it is a plaintext value, a BCrypt/MD5 hash, a truncated hash, or a hash produced with a different variant like $argon2i instead of $argon2id).

Common situations: Migrating a legacy user database whose password column holds non-Argon2 hashes; a column truncated by a too-short VARCHAR; users whose password was stored plaintext; switching encoders without a DelegatingPasswordEncoder.

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/8d05ffc885502ab7. Report an issue: GitHub.

Appendix: source

Thrown at crypto/src/main/java/org/springframework/security/crypto/argon2/Argon2PasswordEncoder.java:136

				.withParallelism(this.parallelism)
				.withMemoryAsKB(this.memory)
				.withIterations(this.iterations)
				.build();
		// @formatter:on
		Argon2BytesGenerator generator = new Argon2BytesGenerator();
		generator.init(params);
		generator.generateBytes(rawPassword.toString().toCharArray(), hash);
		return Argon2EncodingUtils.encode(hash, params);
	}

	@Override
	protected boolean matchesNonNull(String rawPassword, String encodedPassword) {
		Argon2EncodingUtils.Argon2Hash decoded;
		try {
			decoded = Argon2EncodingUtils.decode(encodedPassword);
		}
		catch (IllegalArgumentException ex) {
			this.logger.warn("Malformed password hash", ex);
			return false;
		}
		byte[] hashBytes = new byte[decoded.getHash().length];
		Argon2BytesGenerator generator = new Argon2BytesGenerator();
		generator.init(decoded.getParameters());
		generator.generateBytes(rawPassword.toString().toCharArray(), hashBytes);
		return constantTimeArrayEquals(decoded.getHash(), hashBytes);
	}

	@Override
	protected boolean upgradeEncodingNonNull(String encodedPassword) {
		Argon2Parameters parameters = Argon2EncodingUtils.decode(encodedPassword).getParameters();
		return parameters.getMemory() < this.memory || parameters.getIterations() < this.iterations;
	}

	private static boolean constantTimeArrayEquals(byte[] expected, byte[] actual) {
		if (expected.length != actual.length) {
			return false;

View on GitHub (pinned to 96852e8860)