Tiger_Security

@api

Tiger_Security — the application PEPPER (a keyed secret mixed into hashes).

SALTS are already handled per-record: bcrypt (password_hash) mints a random salt per password. A PEPPER is different — one secret, shared by the whole install, kept OUT of the database (local.ini / a secrets manager), and HMAC'd into a value before it's hashed. The payoff: a stolen user/user_credential table is useless on its own — without the pepper you can't even start cracking, because every stored hash was keyed by a secret the DB never held. It also lifts short, low-entropy codes (a 6-digit OTP, a 10-char recovery code) out of offline brute-force range.

ROTATION. Because a hash is ONE-WAY you can't re-pepper it without the plaintext, so the pepper rotates LAZILY: the CURRENT pepper is tiger.security.pepper; the previous one(s) go to tiger.security.pepper_retired (comma-separated). New hashes use the current pepper; VERIFY tries current-then-retired(-then-legacy-raw) and re-hashes to the current pepper on any non-current match — so passwords migrate as users sign in, and the retired pepper can be dropped once traffic has turned everyone over (force-reset any stragglers). Contrast Tiger_Crypto, whose reversible data is re-encrypted eagerly.

With NO pepper configured every method degrades to exactly the legacy behavior (password_hash($p) / hash('sha256', $code)), so existing installs are unaffected.

Methods

prehashPassword()

prehashPassword($plain)

Prepare a password for password_hash(): HMAC-then-base64 with the CURRENT pepper, else the raw password (no-pepper path). base64 of the 32-byte HMAC is 44 chars — under bcrypt's 72-byte limit and NUL-safe (so long passwords aren't truncated).

  • $plain string — the plaintext password

Returns string — the pepper-prepared (or raw) password to hash

passwordVerifiers()

passwordVerifiers($plain)

Every password form to try on VERIFY, in order: current pepper, each retired pepper, then legacy raw. The caller matches against the stored bcrypt hash and, if the match is NOT the first (current) form, re-hashes to the current scheme.

  • $plain string — the plaintext password to build candidates for

Returns string[]

hashCode()

hashCode($code, $context = '')

Keyed hash for short secret CODES (OTP / reset / recovery), using the CURRENT pepper (per-context subkey), else plain sha256. $context domain-separates so the same code hashes differently as a recovery code vs a login challenge.

  • $code string — the short secret code to hash
  • $context string — domain-separation label (e.g. 'recovery')

Returns string — 64-char hex (same shape/length as the legacy sha256).

codeMatches()

codeMatches($code, $context, $storedHash)

Constant-time check of a code against a stored hash, trying the current pepper, each retired pepper, then the legacy sha256 — so a code issued before a rotation still redeems. (Transient codes could just expire, but recovery codes are longer-lived.)

  • $code string — the code to check
  • $context string — the domain-separation label used when it was hashed
  • $storedHash string — the stored hash to compare against

Returns bool — true on a match under any (current/retired/legacy) pepper

hasPepper()

hasPepper()

Is a pepper configured? (Drives the graceful-migration fallbacks in the callers.)

Returns bool

generatePepper()

generatePepper()

Mint a fresh pepper for local.ini / a secrets manager (install + rotate).

Returns string — a new base64-encoded 32-byte pepper