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).
$plainstring— 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.
$plainstring— 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.
$codestring— the short secret code to hash$contextstring— 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.)
$codestring— the code to check$contextstring— the domain-separation label used when it was hashed$storedHashstring— 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