Class PasswordHashHelper

java.lang.Object
org.codelibs.fess.helper.PasswordHashHelper

public class PasswordHashHelper extends Object
Facade for password hashing and verification.

Implements a prefix-based ({id}hash) delegating scheme that is functionally equivalent to Spring Security's PasswordEncoderFactories.createDelegatingPasswordEncoder() (v5.8 defaults), without pulling in any external dependency. The default encoder is BCrypt (cost 10, $2a$) and legacy pre-prefix hashes are still verifiable via app.digest.algorithm (sha256/sha512/md5 hex lower-case, no salt).

  • Field Details

    • PREFIX

      protected static final String PREFIX
      Prefix marker (start) of the encoder id.
      See Also:
    • SUFFIX

      protected static final String SUFFIX
      Prefix marker (end) of the encoder id.
      See Also:
    • ID_BCRYPT

      protected static final String ID_BCRYPT
      Encoder id for BCrypt.
      See Also:
    • BCRYPT_PREFIX

      public static final String BCRYPT_PREFIX
      Full id prefix token for BCrypt ({bcrypt}).
      See Also:
    • DUMMY_BCRYPT_SEED

      protected static final String DUMMY_BCRYPT_SEED
      Plaintext seed used to generate the dummy BCrypt hash consumed by applyTimingPadding(). The value is not secret and is not user-supplied; it exists only to produce a well-formed hash for timing-attack equalisation.
      See Also:
    • encoders

      protected volatile Map<String,PasswordHashHelper.PasswordEncoder> encoders
      Registered encoders keyed by id. Populated lazily and read-only after init.
    • dummyBcryptHash

      protected volatile String dummyBcryptHash
      Cached dummy BCrypt hash used by applyTimingPadding() as a timing-attack countermeasure. Built at container start-up (see init()) using the currently configured app.password.bcrypt.cost, so that the dummy path uses the same BCrypt cost as real user records. Stored in a volatile field with double-checked locking so a runtime cost bump (via config reload) can still trigger a one-time regeneration without synchronisation on the hot path.
  • Constructor Details

    • PasswordHashHelper

      public PasswordHashHelper()
      Default constructor.
  • Method Details

    • init

      @PostConstruct public void init()
      Validates the password configuration eagerly at container start-up so that a misconfigured app.password.algorithm fails fast instead of surfacing on the first user login. Also forces the encoder map to build so its construction cost is not paid on the login critical path.
    • applyTimingPadding

      public void applyTimingPadding()
      Consumes approximately one BCrypt verification worth of CPU to equalise authentication latency across success and failure paths. Callers should invoke this on the failure branch when the normal matches(java.lang.String, java.lang.String) invocation did not already pay a BCrypt cost (for example: user not found, legacy hex-digest stored value, unknown-id prefix).

      This method never throws: if the dummy hash cannot be produced for any reason, it returns silently rather than leaking a distinguishable exception to the caller.

    • isTimingSafeHash

      public boolean isTimingSafeHash(String storedPassword)
      Indicates whether the supplied stored hash is in a format whose verification via matches(String, String) already pays a BCrypt cost (or equivalent), meaning the caller does not need to add applyTimingPadding() on the failure branch.
      Parameters:
      storedPassword - the stored (hashed) password value
      Returns:
      true if the id prefix denotes a timing-safe encoder (currently only {bcrypt}); false for legacy unprefixed values and unknown prefixes
    • getDummyBcryptHash

      protected String getDummyBcryptHash()
      Returns the dummy BCrypt hash used by applyTimingPadding(), generating it lazily on first access using the configured BCrypt cost. Uses double-checked locking so subsequent calls are lock-free.
      Returns:
      a valid {bcrypt}$2a$... string that will never match any real user-supplied plaintext, or null if generation failed (logged only)
    • resolveBcryptCost

      protected int resolveBcryptCost()
      Resolves the effective BCrypt cost, clamped into the jBCrypt-valid range [4, 30]. Extracted from PasswordHashHelper.BcryptPasswordEncoder so the dummy-hash path uses the exact same value as real user records.
      Returns:
      the effective BCrypt cost in the range [4, 30]
    • encode

      public String encode(String rawPassword)
      Encodes the raw password using the configured default algorithm.
      Parameters:
      rawPassword - the plain-text password (must not be null)
      Returns:
      the encoded password prefixed with {id}
      Throws:
      NullPointerException - if rawPassword is null
      IllegalStateException - if the configured algorithm is unknown
    • matches

      public boolean matches(String rawPassword, String storedPassword)
      Verifies that the raw password matches the stored representation. Falls back to legacy hex digest verification when the stored value carries no {id} prefix.
      Parameters:
      rawPassword - the plain-text password being verified
      storedPassword - the stored (hashed) password
      Returns:
      true if the password matches, false otherwise (including null/empty inputs and unknown prefixes)
    • upgradeEncoding

      public boolean upgradeEncoding(String storedPassword)
      Determines whether the stored password should be re-encoded using the currently configured algorithm and parameters.
      Parameters:
      storedPassword - the stored (hashed) password
      Returns:
      true if re-encoding is recommended
    • resolveIdForEncode

      protected String resolveIdForEncode()
      Resolves the encoder id used for new passwords.
      Returns:
      the configured encoder id (lower-cased)
    • extractId

      protected String extractId(String storedPassword)
      Extracts the {id} from a stored password value, or null if it does not carry a prefix.
      Parameters:
      storedPassword - the stored password value (possibly prefixed)
      Returns:
      the id inside {...}, or null if not prefixed
    • parseBcryptCost

      protected int parseBcryptCost(String bcryptHash)
      Parses the cost (log rounds) component of a BCrypt hash ($2a$NN$...).
      Parameters:
      bcryptHash - the BCrypt hash string (without any {id} prefix)
      Returns:
      the cost value, or -1 if parsing fails
    • matchesLegacy

      protected boolean matchesLegacy(String rawPassword, String storedPassword)
      Verifies a legacy (unprefixed) hex digest using app.digest.algorithm with a constant-time comparison.
      Parameters:
      rawPassword - the plain-text password
      storedPassword - the stored legacy hex digest
      Returns:
      true if matched
    • toJcaDigestName

      protected String toJcaDigestName(String algorithm)
      Translates the short algorithm id (sha256/sha512/md5) recorded in app.digest.algorithm into the JCA standard name.
      Parameters:
      algorithm - the short algorithm id
      Returns:
      the JCA digest name, or null if unsupported
    • hexDecodeLowerCase

      protected byte[] hexDecodeLowerCase(String hex)
      Decodes a lower-case hex string into bytes. Upper-case input is also accepted for robustness — constant-time comparison still succeeds.
      Parameters:
      hex - the hex-encoded input
      Returns:
      the decoded bytes, or null if the input is not a valid even-length hex string
    • getEncoders

      protected Map<String,PasswordHashHelper.PasswordEncoder> getEncoders()
      Returns the encoder map, initializing it lazily on first access.
      Returns:
      an immutable map of encoder id to encoder