Skip to content

password-hash 0.6 Argon2 verification fails for PHC hashes with non-default output lengths #2352

@lovasoa

Description

@lovasoa

Hi!

argon2 0.6.0-rc.8 can generate a valid PHC string with a non-default output length, but
verifying that same PHC string via Argon2::verify_password fails with PasswordInvalid.

This looks like the password-hash 0.6 verification path rebuilds the hash from the PHC params
without preserving the encoded output length from the parsed PHC string.

Versions

  • argon2 = 0.6.0-rc.8
  • transitive password-hash = 0.6.0
  • Rust toolchain: rustc 1.94.1

Minimal repro

Cargo.toml

[package]
name = "argon2-output-len-repro"
version = "0.1.0"
edition = "2024"

[dependencies]
argon2 = "=0.6.0-rc.8"

src/lib.rs

#[cfg(test)]
mod tests {
    use argon2::{
        Algorithm, Argon2, Params, PasswordHash, PasswordHasher, PasswordVerifier, Version,
    };

    const PASSWORD: &[u8] = b"password";
    const SALT: &[u8] = b"aaaaaaaa";

    #[test]
    fn default_output_len_round_trip_still_verifies() {
        let hash = Argon2::default()
            .hash_password_with_salt(PASSWORD, SALT)
            .unwrap()
            .to_string();
        let parsed = PasswordHash::new(&hash).unwrap();

        assert_eq!(Argon2::default().verify_password(PASSWORD, &parsed), Ok(()));
    }

    #[test]
    fn non_default_output_len_round_trip_should_verify() {
        let params = Params::new(8, 1, 1, Some(16)).unwrap();
        let hash = Argon2::new(Algorithm::Argon2id, Version::V0x13, params)
            .hash_password_with_salt(PASSWORD, SALT)
            .unwrap()
            .to_string();
        let parsed = PasswordHash::new(&hash).unwrap();

        assert_eq!(Argon2::default().verify_password(PASSWORD, &parsed), Ok(()));
    }
}

Reproduction steps

cd repro/argon2-output-len-repro
cargo test

Expected behavior

Both tests pass. In particular, a PHC string generated by argon2 0.6.0-rc.8 with
Params::new(8, 1, 1, Some(16)) should verify successfully when parsed back and checked with
verify_password.

Actual behavior

The default-output-length test passes, but the non-default-output-length round trip fails with
PasswordInvalid.

Verified locally with:

running 2 tests
test tests::non_default_output_len_round_trip_should_verify ... FAILED
test tests::default_output_len_round_trip_still_verifies ... ok

---- tests::non_default_output_len_round_trip_should_verify stdout ----
thread 'tests::non_default_output_len_round_trip_should_verify' panicked at src/lib.rs:30:9:
assertion `left == right` failed
  left: Err(PasswordInvalid)
 right: Ok(())

Notes

This also reproduces in a larger application when:

  • verifying a stored Argon2 PHC hash whose encoded hash length is shorter than the default 32 bytes
  • verifying OIDC nonce hashes generated with output_len = 16

My working theory is:

  1. password-hash 0.6 parses the PHC string correctly.
  2. The generic verifier reconstructs the algorithm parameters from hash.params.
  3. Argon2 Params::try_from(&hash.params) does not recover the encoded hash output length, because
    that length is carried by hash.hash, not the params string.
  4. Verification recomputes the hash with the default output length (32) and returns
    PasswordInvalid.

I tested this locally from SQLPage while evaluating an upgrade to argon2 0.6.0-rc.8.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions