Secure Password Storage with PHP

Getting started – Password security

A common issue that I find with many tutorials and other programmers’ code is that passwords are stored in the database in an insecure way. If someone gains access to the database, you can prevent much of the potential damage by storing the passwords in a way that won’t be of much use to a hacker. Normal hashing functions don’t really accomplish this. Hackers can use a Rainbow Table to try to identify what the password was before the password was hashed. The best way to store a password is to salt the password. This is a technique that adds a unique random string to the beginning or end of each password. This technique requires that a hacker compute an entire hash table to crack a single password, already a much more difficult task than using a cleartext or MD5 hashed password. Lastly, if we use a costly algorithm to hash the passwords initially, it becomes computationally unreasonable for someone to compute a rainbow table for a salted password. PHP has some great tools for performing these functions, so we might as well use them.

Hashing

If we run the following code we can see the MD5 hash for the word “password”:


echo md5("password");

outputs: 5f4dcc3b5aa765d61d8327deb882cf99

Say someone gains access to your database and they are able to retrieve this hash. If they run every dictionary word through an MD5 hashing algorithm, they generate what is called a rainbow table, or they can use one of the freely available rainbow tables online. You simply need to compare the hash that you received from the database with the hash in the rainbow table to determine that the original string was “password” (like with this online tool). Now they can gain access to the account, and likely many others that use the same username password pair. This is why you should salt passwords. Let’s say again that a user chose “password” to authenticate with but this time we salt the password before storing it in the database.


$salt = "nzxcvhas871kjh1nasdfk891234";
echo md5($salt . "password");
outputs: 545cc4c9e738b5572bc8e7c6d82ebe54

You’ll notice that we now have a completely different hash. This means that unless the rainbow table is very exhaustive (unfortunately for MD5, they are, which I will address in a bit), this hash will not be a pre-computed one. Even if the hacker knows the salt (which they will if they have database access), they will have to recompute a new rainbow table for each password that they want to break.

Costly Hashing Algorithms

Something we can do to increase the security of passwords stored in the database, is make the hashes more costly to compute. MD5 is not a very computationally complex hashing algorithm, so running it on every dictionary word with a prepended salt is not unfeasible, and there are some distributed tools to help speed up this process. As an authentication provider, we only need to compute the hash once for each login. If we use a hashing algorithm that takes an extra half second each time a user logs in, they will barely notice a difference, however, if you have to run that hashing function millions of times to create a rainbow table for each password you want to crack, it becomes computationally unfeasible. So, we can salt passwords so their rainbow tables will be unique, and we can use a costly algorithm so that computing a unique table for each salt is basically impossible. 

Doing it with PHP

PHP version 5.5.0 and above have a great set of  functions called password_hash and password_verify that are very secure and handle salt generation and algorithm selection for you. This basically allows you to take a user’s passed password (like $_POST[‘password’]) and put it through password_hash before putting it into the database. When it comes time to authenticate them, you pull the hash out of the database and provide password_verify with the clear text password and the hash and it will return a boolean indicating a match.

If you don’t have PHP 5.5.0 or greater, it requires a little more effort, but not much. We can replicate the functionality of password_hash and password_verify pretty easily. Let’s say we are creating a new user based on a posted username and password


//Generate the hash
$hash = crypt($_POST['password']);
echo $hash;
outputs: $1$oWVVgqRq$mfPRV7KC6NWmmAV4kfeXU/

Your script will output a different string, in fact it will output a different string each time you run it. PHP’s crypt function will generate a salt each time it is run, and this can change depending on your operating system. I strongly recommend that you read the crypt documentation to determine if you should be generating your own salt and provide it as an optional parameter. Some operating systems will default to a 2 character salt, which is pretty weak, so you might want to specify a different hashing algorithm with the functions options.

So now you have a hash, what happened to the salt?
$1$oWVVgqRq$mfPRV7KC6NWmmAV4kfeXU/

The hash contains some options (in blue), the salt generated (in red), and the algorithm’s hash of the password (in black). We want to save the whole thing in the database because we will use all of the components to authenticate a user. At this point, you would also save the username in the database.

Authenticating an existing user

When a user tries to login, we are probably going to call some function that boils down to a query like this:

$query = "SELECT * FROM users_table WHERE username=:username";

The result of this query will pass back the password hash. So when a user logs in, to authenticate them, we should do something like this:


//Pull the salt out of the full hash
$salt = substr($password_hash,0,15);
if(crypt($_POST['password'],$salt) == $password_hash) {
  $authed = true;
  //Header to some protected page
}
else {
  //Some kind of error handling, probably not die() but you get the point
  die("Invalid username password pair");
}

Leave a Reply

Your email address will not be published. Required fields are marked *