PHP password_hash, password_verify, and hash_hmac

Store passwords with bcrypt through the built-in API and sign messages with HMAC.

PHP
<?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";
Output
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
Guided walkthroughReady
1<?php
2$hash = password_hash("correct horse", PASSWORD_DEFAULT);
3echo "algorithm: ", password_get_info($hash)["algoName"], "\n";
4echo "right password: ", password_verify("correct horse", $hash) ? "accepted" : "rejected", "\n";
5echo "wrong password: ", password_verify("wrong guess", $hash) ? "accepted" : "rejected", "\n";
6echo "hashes differ for equal passwords: ",
7 password_hash("correct horse", PASSWORD_DEFAULT) !== $hash ? "yes" : "no", "\n";
8
9$tag = hash_hmac("sha256", "what do ya want for nothing?", "Jefe");
10echo $tag, "\n";
11$again = hash_hmac("sha256", "what do ya want for nothing?", "Jefe");
12echo "valid: ", hash_equals($tag, $again) ? "yes" : "no", "\n";
13$forged = hash_hmac("sha256", "what do ya want for something?", "Jefe");
14echo "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
Prepared trace of the published program. Some rows group repeated iterations.
Step / lineWhat happensState after stepOutput 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.

Further reading