Password Hashing in PHP

Password hashing is better than message digests or encryption, and you should be using it instead of message digests or encryption.  Find out why and how in this article, which supplements the original article on PHP Client Registration, Login, Logout, and Easy Access Control.
Password hashing, introduced at PHP 5.5*, offers a better protection strategy than encryption or md5().  This article tells a brief history of stored passwords, and shows why existing password protection strategies have often failed.  The article shows how to use hashing as a better way forward.

A Basic Client Authentication Design
PHP client authentication is described in the E-E article on PHP Client Registration, Login, Logout, and Easy Access Control.  The original article was published many years ago, and was intended to show the skeleton design pattern of PHP client authentication.  These authentication mechanisms are common to all "client login" sites, but any single article on highly detailed and technical subjects cannot show everything.  Over the years many visitors have pointed out that the original article does not show how to obscure or protect the client passwords.  And for security reasons we want to be sure that we do not store client passwords in clear text.  This article, new in 2016, is intended to supplement and improve upon the original by showing the current best practices for password storage and verification.  We will look at the history of password storage, some techniques that once worked, but have been overtaken by events, and the state of the art today.

What Changed Over the Years
Once upon a time, in the 1990's, we used to store client user-ids in clear-text, and once upon a time, that was OK because nobody was doing anything really important with the internet yet.  We were just feeling our way in the dark.  However we quickly saw the promise of eCommerce and the value of secure and authenticated communications.  So we added a single secret password to our designs.  And for a while that was good enough.  But quickly, hacking became a "thing" and some institutions lost control of their client databases.  When our databases got compromised, all of the clear-text user-ids and passwords were exposed, often with horrible results for the victims.  The industry began to realize that better security precautions were needed.

Using PHP md5() to Obscure Passwords (nope)
PHP powers an overwhelming number of web sites, and early attempts to protect client passwords often originated in PHP scripts.  The earliest upgrades to our concept of password storage used the MD5 algorithm to encode the passwords.  PHP implemented this algorithm with the md5() function.  The MD5 algorithm creates a "message digest" from a data string.  The PHP md5() digest is a hexadecimal number, 32 bytes long, and it's a hash, not an encryption (these terms are different and the difference matters).  Since it is a hash, the md5() output was not readily reversible.  MD5 gave us a way to store an encoded password string that obscured the original password.

The client registration process would take the incoming password, encode it with md5() and store the resulting message digest in the database, along with the client id.  The login process would take the incoming password, encode it with md5() and compare the resulting message digest to the one stored in the database.  If they match, the password was correct.  And if the hackers got our database, they did not get the passwords - all they got were the irreversible hexadecimal message digests.

But the md5() algorithm is idempotent, meaning that any given password will always produce the same md5() message digest.  For example, md5("password") always produces "5f4dcc3b5aa765d61d8327deb882cf99."  It follows that if two of your clients choose the same password, they will have the same md5() values stored in your client database.  This phenomenon led to "rainbow" attacks, in which popular passwords were fed to the md5() function, and the resulting message digests were stored in the hackers' database.  Now the hackers could use their collection of md5() message digests to look up the original passwords that produced these message digests, and armed with the clear-text passwords, they could simply log into the web application and impersonate their victims.  It is computationally trivial to store and retrieve the md5() values for hundreds of thousands, or even millions, of common words and phrases.  This meant that md5() alone would not suffice.

Using PHP md5() with a Salt String (nope)
Our initial response to the rainbow attack was to add a "salt" value to the client's password string.  If the client gave us "password" we might store the md5("passwordXYZ") in the client database.  Then at the time a client tries to log in, we would add the XYZ salt to the end of whatever they entered for their password, compute the md5() message digest, and use the resulting string in our database lookup.

This concept worked fairly well, so long as a few things remained true.  First, our "salt" string needs to be long, complicated and obscure.  Second, we might need to add more than one salt string (maybe call these "salt and pepper").  Third, our salt and pepper strings must remain secret, because once they are exposed, it again becomes computationally trivial for our attackers to use the rainbow attack method against our database.  Obviously there is a flaw in this scheme: The salt and pepper strings must exist somewhere in clear text!  We must use them during the client authentication process.

The risk is this: If hackers get our client database they may also get our PHP scripts, and if they get our PHP scripts they may be able to discern our salt and pepper strings.  And once they have that information, we've compromised our clients once again.  The hackers would now be able to reverse engineer the passwords.  This makes it possible that the hackers can selectively and surreptitiously impersonate one or more of our clients, performing unauthorized transactions in secrecy and at will.

Using PHP md5() with Variable Salt Strings (nope)
So our secondary response was to use a different salt value for every client.  Usually this took the form of extracting at least part of the salt from the user-id or a similarly predictable data value.  This was better (identical passwords would no longer had identical MD5 strings), but was still subject to reverse engineering if the PHP scripts were exposed.

Using Encryption (nope)
Another tack, tried and failed, was to use encryption to obscure the passwords.  Encryption requires more computational resources to break when compared to hashing.  But encryption is reversible, and eventually it will be broken.  If we're storing high-value information and the attackers know that we have encrypted our passwords, then we will become a target.  Encryption is not appropriate for password storage.

Looking for Better Answers (maybe)
All kinds of discussions ensued about "better" ways of obscuring passwords.  One of the best ideas is to avoid passwords entirely, and use a pass-phrase instead.  Correct Horse Battery Staple became a term of art.  But most existing web sites never made the change to a pass-phrase, or even to encryption.  To understand why, read the professional literature about hashing and encryption.  You need an advanced understanding of mathematics to understand encryption, and it's not really a very good solution.  We needed something that was both secure and simple to use.

At PHP 5.5, Anthony Ferrara and others introduced the concept of simplified password hashing and verification.  As a result, most of the article discussion about An Afterword: About Storing Passwords is now obsolete; there are better ways of dealing with the issues surrounding password secrecy and protection.

Today, we do it this way. 

When a client registers a new password, we use password_hash() to get a hash string.  We store this string in our database, using a column that allows expansion to 255 characters.

When a client attempts to login, we look up the client record by user-id and get the hash string from the database.  Then we test the input password against the hash string with password_verify().  The answer is either True or False.

It's important that we use password_verify() to test the input password against the stored hash.  Even when using identical passwords, different calls to password_hash() may produce different hash strings.  For this reason, direct comparisons of the hash strings are not usable.  You cannot use the hash string in a database query.

Going Forward
While this scheme is far more secure than md5() or encryption, and programmatically simpler, there are some things in the design of our code that we have to be aware of.  The most obvious is that we cannot authenticate a client on the basis of a database lookup alone.  Instead we must use a two-step process, first locating the user-id (perhaps an email address) then verifying the password.  This implies that the user-id must be UNIQUE in the SQL database, or if we deliberately permit duplicate user-ids, we must iterate over the collection of identical user-ids, testing each of the passwords until one or none of them is verified.  My sense is that choosing unique user-ids is a better way to go.  Since email addresses are unique in the universe, they play well with the idea of unique user-ids.

A less obvious issue comes up when the client wants to change passwords or re-authenticate.  We can't look up the old password, nor even look up the hash of the old password.  Instead, we must look up the user's record by user-id, pull out the hash, and use password_verify() with the client's input password.  If these match, we can allow a password change or establish re-authentication.

How to Write the PHP Code
Let's look at some code examples that use the PHP 5.5 hashing and verification. (Author note: password_hash() output is a BCrypt hash, and is therefore already base64 encoded.)

Here's an example that shows how to use a Password Class with static methods for hashing and verification.
<?php // demo/password_static_hashing.php
                       * Show how to hash and verify a password
                       * Store the result in a database column that can expand to 255 characters
                      class Password
                          public static function hash($pass, $algo=PASSWORD_DEFAULT)
                              $text = trim($pass);
                              return password_hash($pass, $algo);
                          public static function verify($pass, $hash)
                              return password_verify($pass, $hash);
                      $pass = $hash = NULL;
                      // IF ANYTHING WAS POSTED SHOW THE DATA
                      if (!empty($_POST['pass']))
                          $pass = $_POST['pass'];
                          $hash = Password::hash($pass);
                          echo "<br/>PASSWORD <b>$pass</b> YIELDS HASH ";
                          echo "<i>$hash</i>";
                      if (!empty($_POST['hash']))
                          $result = Password::verify($_POST['pass'], $_POST['hash']);
                          if  ($result) echo "<br/>PASSWORD <b>{$_POST['pass']}</b>       PASSES        VERIFICATION WITH HASH <i>{$_POST['hash']}</i> ";
                          if (!$result) echo "<br/>PASSWORD <b>{$_POST['pass']}</b> <b><i>FAILS</i></b> VERIFICATION WITH HASH <i>{$_POST['hash']}</i> ";
                      $form = <<<FORM
                      <style type="text/css">
                      .txt { width:60em; }
                      <form method="post">
                      <input class="txt" name="pass" value="$pass" autocomplete="off" />
                      <input type="submit" value="HASH THIS PASSWORD" />
                      <input class="txt" name="hash" value="$hash" autocomplete="off" />
                      <input type="submit" value="VERIFY $pass WITH THIS HASH" />
                      echo $form;

Open in new window

Here's an example showing that password_hash() creates irreproducible results.  Any of these hash strings will yield a True result when compared to the original with password_verify().  In my tests, I found that you could re-hash the same password thousands of times without generating a duplicate hash.  Yet all of the hash strings work correctly with password_verify().
<?php // demo/password_hashing_repeated.php
                       * Show that repeated password_hash() generates different outputs
                      $pass  = 'password';
                      $hash1 = password_hash($pass, PASSWORD_DEFAULT);
                      $hash2 = password_hash($pass, PASSWORD_DEFAULT);
                      $hash3 = password_hash($pass, PASSWORD_DEFAULT);
                      $out = <<<EOD
                      <p>PHP password_hash('$pass') yields
                      echo $out;
                       * Outputs something like:
                       * PHP password_hash('password') yields
                       * $2y$10$TN/d3rluRa7/RJwfsZAFu.aT6D0UM/1iBG1G6QU0V3Vv8h9ENiaQK
                       * $2y$10$5mJ8SNCYJgf2WNGM9lOl5u5jna9FOSJ82Rm6zpZvZmtRyXjrbr1u.
                       * $2y$10$YjUxugn.8nYDq15H.MFGaeH1MEJrJCpvvo7Y172LIDV1fXDYxaNXu

Open in new window

* What if I'm not using PHP 5.5+?
You should upgrade your PHP installation!  But for those stuck with an older version of PHP, there is a workaround available here:

Password protection remains one of our strongest defenses against unwanted intrusion and data compromise.  The current state of the art in PHP gives us an easy-to-use implementation of password hashing, and helps protect our online resources against loss or damage, even if the client database is accidentally exposed.  It's important to review our techniques for password storage and verification, and if we're using one of the older and less secure techniques, it's important to upgrade now, before an attack succeeds.


Please give us your feedback!
If you found this article helpful, please click the "thumb's up" button below. Doing so lets the E-E community know what is valuable for E-E members and helps provide direction for future articles.  If you have questions or comments, please add them.  Thanks!

Comments (0)

Have a question about something in this article? You can receive help directly from the article author. Sign up for a free trial to get started.