PHP password_hash, password_verify, and hash_hmac
Store passwords with bcrypt through the built-in API and sign messages with HMAC.
<?php
$hash = password_hash("correct horse", PASSWORD_DEFAULT);
echo "algorithm: ", password_get_info($hash)["algoName"], "\n";
echo "right password: ", password_verify("correct horse", $hash) ? "accepted" : "rejected", "\n";
echo "wrong password: ", password_verify("wrong guess", $hash) ? "accepted" : "rejected", "\n";
echo "hashes differ for equal passwords: ",
password_hash("correct horse", PASSWORD_DEFAULT) !== $hash ? "yes" : "no", "\n";
$tag = hash_hmac("sha256", "what do ya want for nothing?", "Jefe");
echo $tag, "\n";
$again = hash_hmac("sha256", "what do ya want for nothing?", "Jefe");
echo "valid: ", hash_equals($tag, $again) ? "yes" : "no", "\n";
$forged = hash_hmac("sha256", "what do ya want for something?", "Jefe");
echo "forged valid: ", hash_equals($tag, $forged) ? "yes" : "no", "\n";algorithm: bcrypt right password: accepted wrong password: rejected hashes differ for equal passwords: yes 5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843 valid: yes forged valid: no
Explanation
PHP has a ready-made, safe way to store passwords. password_hash creates a salted hash with a slow algorithm (bcrypt by default). The result contains the algorithm, the cost, and the random salt, so it can be stored in one database column, and password_verify checks a login attempt against it. Never write your own password scheme with md5 or sha1.
As hardware gets faster, the cost should go up. password_needs_rehash tells you after a successful login that the stored hash uses an old setting, so you can store a new one. hash_hmac computes a keyed hash for message authentication, and hash_equals compares two strings in constant time, which prevents timing attacks. The tag below is the published RFC 4231 test value for the key Jefe.
Explore the dry run
<?php$hash = password_hash("correct horse", PASSWORD_DEFAULT);echo "algorithm: ", password_get_info($hash)["algoName"], "\n";echo "right password: ", password_verify("correct horse", $hash) ? "accepted" : "rejected", "\n";echo "wrong password: ", password_verify("wrong guess", $hash) ? "accepted" : "rejected", "\n";echo "hashes differ for equal passwords: ", password_hash("correct horse", PASSWORD_DEFAULT) !== $hash ? "yes" : "no", "\n"; $tag = hash_hmac("sha256", "what do ya want for nothing?", "Jefe");echo $tag, "\n";$again = hash_hmac("sha256", "what do ya want for nothing?", "Jefe");echo "valid: ", hash_equals($tag, $again) ? "yes" : "no", "\n";$forged = hash_hmac("sha256", "what do ya want for something?", "Jefe");echo "forged valid: ", hash_equals($tag, $forged) ? "yes" : "no", "\n";Follow the example
Start to inspect the source, values, and output. These prepared steps explain the published sample; grouped loop steps describe multiple iterations.
View the complete dry-run table
| Step / line | What happens | State after step | Output so far |
|---|---|---|---|
| A salted bcrypt hash. The salt and the cost are stored inside the string. | — | — | |
| The hash records which algorithm made it. | — | algorithm: bcrypt | |
| password_verify recomputes the hash using the stored salt and compares in constant time. | — | algorithm: bcrypt right password: accepted | |
| A different password does not match. | — | algorithm: bcrypt right password: accepted wrong password: rejected | |
| A fresh random salt gives a different hash for the same password. | — | algorithm: bcrypt right password: accepted wrong password: rejected hashes differ for equal passwords: yes | |
| The RFC 4231 test vector for HMAC-SHA256 with key Jefe. | — | algorithm: bcrypt right password: accepted wrong password: rejected hashes differ for equal passwords: yes 5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843 | |
| hash_equals compares in constant time. | — | algorithm: bcrypt right password: accepted wrong password: rejected hashes differ for equal passwords: yes 5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843 valid: yes | |
| A changed message gives a different tag, so the forgery is rejected. | — | algorithm: bcrypt right password: accepted wrong password: rejected hashes differ for equal passwords: yes 5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843 valid: yes forged valid: no |
Cost and common mistakes
password_hash is deliberately slow, tuned by the cost parameter. hash_hmac is O(n) in the message length.
- Never compare hashes or tags with == or ===. Use hash_equals so the comparison time does not leak information.
- bcrypt only uses the first 72 bytes of a password. Very long passphrases may need pre-hashing, or use PASSWORD_ARGON2ID if your PHP build supports it.
- md5() and sha1() are not password hashes. They are fast, which helps attackers.
Try it yourself
When should password_needs_rehash be called?
Reveal answer and reasoning
After a successful login, using the stored hash. If it returns true, hash the password again with the current settings and save the new hash.