Skip to content
Flag of Europe
Made in the European Union · Independently built · Released under EUPL 1.2
Credential Hardening

Credential Hardening

Available on Maven Central as part of the 00.72.00 release line. Pull it in with 00.72.00 coordinates (see Quick Start); the optional security-crypto-bc (Argon2id/bcrypt/scrypt) and security-credentials-hibp (breached-password check) modules are strictly opt-in.

v00.71.00 adds a dedicated credential-security stack built against OWASP ASVS V2, NIST SP 800-63B and 40 CWE weakness classes (see Compliance & CWE Coverage). Every public verification result is a sealed type — the boolean-only shape of the older PasswordHasher is gone from the new code paths.

Two new strictly opt-in modules join the reactor (15 → 16). An application that doesn’t depend on them never pulls in BouncyCastle and never makes an outbound HTTP call.

Pluggable hashing

AlgorithmModuleDefault parameters
PBKDF2-HMAC-SHA256security-core (JDK-only)600 000 iterations, 16-byte salt, 32-byte key (OWASP-2023 floor)
Argon2idsecurity-crypto-bc (opt-in)t=3, m=64 MiB, p=1, 32-byte output
bcryptsecurity-crypto-bc (opt-in)cost=12; 72-byte input limit rejected explicitly (no silent pre-hashing)
scryptsecurity-crypto-bc (opt-in)N=2¹⁵, r=8, p=1; memory cost overflow-checked
// JDK-only default
PasswordHashingService svc = PasswordHashingServices.defaults();

// Opt-in modern profile (Argon2id preferred)
PasswordHashingService modern = BouncyCastleHashingServices.modern();

security-crypto-bc uses the BouncyCastle lightweight API only — it never mutates the global JCA provider order. Requesting the modern profile without the module on the classpath fails fast at construction; there is no silent downgrade to a weaker algorithm (CWE-693).

Self-describing envelope

Hashes are stored in a versioned, self-describing wire format so the algorithm, provider, policy version and pepper-key id travel with the hash:

$pwh$v=1$ct=PASSWORD$alg=argon2id$prov=bc$pol=2$pep=k3$p=m=65536,t=3,p=1$h=…

That envelope is what makes crypto-agility work: RehashDecision inspects it on every successful verify and upgrades the hash in place (via compare-and-swap) when the algorithm, parameters, format version, policy version or pepper key have moved on.

Pepper with rotation

PepperApplicator applies an HMAC-SHA-256 pepper after the KDF, with the pepper key held separately from the hash database. A pure DB leak — without the pepper secret — yields hashes that can’t be attacked offline. InMemoryPepperService supports multi-key rotation; rotating the active key triggers a transparent PEPPER_KEY_ROTATED rehash on next login.

Sealed verification results

CredentialVerificationResult result = svc.verify(stored, presented);

switch (result) {
  case CredentialVerificationResult.Verified v -> {
    // v.originalEncodedHash() is the CAS witness for rehash-on-success
    if (v.rehashDecision() instanceof RehashDecision.Required req) {
      store.updateHashIfCurrent(username, svc.hash(presented), v.originalEncodedHash());
    }
  }
  case CredentialVerificationResult.Failed f ->
    // public type collapses to INVALID_CREDENTIALS; internal type goes to audit only
    respondGeneric();
}

For unknown users, call verifyAgainstNothing(...) instead of short-circuiting — the service runs a comparable dummy KDF so login timing stays uniform and accounts can’t be enumerated (CWE-203 / CWE-208).

Secret handling

SecretValue is AutoCloseable with asChars() / asUtf8Bytes() / destroy() and a redacted toString. The hashing-service overloads that take a SecretValue zero the borrowed char[] copy in a finally block. Input goes through a Unicode-aware PasswordNormalizer (NFC) and a PasswordInputPolicy (OWASP-2023 baseline: 8–1024 chars, no composition rules).

Credential lifecycle & atomic persistence

ComponentRole
CredentialStore (SPI)findByUsername, updateHashIfCurrent, updateStatusIfCurrent — every mutation is compare-and-swap (CWE-362)
CredentialStatus8-state machine: ACTIVE, MUST_CHANGE, RESET_PENDING, COMPROMISED, LOCKED, DISABLED, REHASH_REQUIRED, DEPRECATED_ALGORITHM
CredentialLifecycleServicePure decision function → CAS → audit; disallowed transitions throw before touching the store
PasswordChangeService7-step flow with mandatory re-authentication (CWE-620), invalidates other sessions by default (CWE-613)

Password reset — selector/verifier

TokenDigestService issues a 16-byte selector + 32-byte verifier from SecureRandom (Base64URL, no padding), stores only the SHA-256 digest, and compares in constant time. PasswordResetService issues under a TTL, transitions the credential to RESET_PENDING, and consumes single-use via dual compare-and-swap. The perimeter collapses every outcome to one generic response so the reset flow can’t be used to enumerate accounts (CWE-640 / CWE-203).

Abuse detection & breached passwords

  • AbuseDetectionService — multi-dimensional sliding window (USERNAME / CLIENT_ADDRESS / TENANT / GLOBAL) with privacy-minimised credential-stuffing, password-spraying and reset-abuse detectors.
  • ContextAwarePasswordValidator — rejects passwords overlapping the username, email or forbidden terms.
  • CompromisedPasswordChecker (SPI) — NoOp and local-blocklist defaults in core; the opt-in security-credentials-hibp module adds a Have I Been Pwned online check via k-anonymity (only a 5-character SHA-1 prefix leaves the JVM — the plaintext password never does), using the JDK HttpClient with no extra runtime dependency.
  • PasswordHistoryService (opt-in) — blocks reuse of recent passwords.

Operations & emergency response

A FIPS profile, JDK-distribution-trust guidance, SBOM and PKCS#11 HSM notes ship as docs. For incidents, EmergencyPolicyOverride and MassCredentialStatusChange back a set of playbooks (pepper / algorithm / provider compromise, reset abuse, audit review, rollback boundaries).

Audit

Four new sealed AuditEvent variants — CredentialVerificationSucceeded, CredentialVerificationFailed, CredentialRehashed, CredentialStatusChanged — flow through the existing audit pipeline. The new CredentialAuditPublisher swallows sink failures so logging can never break the security flow (CWE-778).

Migration

The V00.71 surface is additive — V00.70 code keeps compiling. To adopt the new stack: switch credential creation to PasswordHashingServices.defaults() (or BouncyCastleHashingServices.modern()), verify via PasswordHashingService.verify(...) and pattern-match the sealed result, call verifyAgainstNothing(...) for unknown users, and feed RehashDecision.Required back through CredentialStore. Bootstrap and demo-rest are already migrated. The experimental pbkdf2$… wire format is not carried over — see the 00.71.00 release notes.