PHP · Course navigation

PHP password_hash, password_verify, and hash_hmac

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

On this page
Before you start

PHP functions and strings.

  • Store passwords with bcrypt through the built-in API and sign messages with HMAC.
  • Predict the sample output and explain the state changes.
  • Complete the practice task and check the boundary cases.

The concept

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.

Code example

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

Follow the code

Open the walkthrough to inspect the example one step at a time.

Show guided walkthrough
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

Behind the scenes

How PHP processes this code

The PHP runtime executes the script. echo writes to the response or terminal output.

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