Credential Hardening
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
| Algorithm | Module | Default parameters |
|---|---|---|
| PBKDF2-HMAC-SHA256 | security-core (JDK-only) | 600 000 iterations, 16-byte salt, 32-byte key (OWASP-2023 floor) |
| Argon2id | security-crypto-bc (opt-in) | t=3, m=64 MiB, p=1, 32-byte output |
| bcrypt | security-crypto-bc (opt-in) | cost=12; 72-byte input limit rejected explicitly (no silent pre-hashing) |
| scrypt | security-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
| Component | Role |
|---|---|
CredentialStore (SPI) | findByUsername, updateHashIfCurrent, updateStatusIfCurrent — every mutation is compare-and-swap (CWE-362) |
CredentialStatus | 8-state machine: ACTIVE, MUST_CHANGE, RESET_PENDING, COMPROMISED, LOCKED, DISABLED, REHASH_REQUIRED, DEPRECATED_ALGORITHM |
CredentialLifecycleService | Pure decision function → CAS → audit; disallowed transitions throw before touching the store |
PasswordChangeService | 7-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) —NoOpand local-blocklist defaults in core; the opt-insecurity-credentials-hibpmodule 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 JDKHttpClientwith 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.