PHP: Everything you need to know about secure password hashing

On each site you need to register, your password has to be saved. This is typically done in a database. There are several ways to do this and some of them are good practice, other a very bad idea.

Lets start with the baaad examples:

The user we have is named "Bob", he likes to use his password "donuts".

Force users to use secure passwords

This is the first point where we have to interact. It does not matter how good our system will encrypt or hash Bobs password, with a simple one like this it does not make sense at all. We need to force Bob to use a more secure password. Good password contain of upper and lowercase letters, numbers and maybe even special chars. But this is not the subject of this article, so we continue with our bad examples.

We forced Bob to use a more secure password like "IlikeD0nuts".

Plain text passwords

The simplest way to store and validate Bobs password would be saving it "as is". The only good thing about this method is, we can send him his password back in case he forgot it. (Hint: If you ever use a "Remember password" function and they send your plain password back, the used some kind of this bad technique.)
The bad thing about this is, if a attacker gets read access to the database, he can read out Bobs password. The attacker is now able to login as Bob and do whatever he wants with Bobs account. If Bob has used the password also for his Email account or anything else the attacker can find out, Bob might get in serious trouble.
As you can see, storing plain text passwords is very bad and should NOT be done.

MD5 / SHA1 passwords

The next better way to store a password, is NOT to store the password. Yes, do NOT store the password, store something that is equivalent to the password but cant be reconverted to the password again.
This one way encryption method is called "hashing". Most famous and versatile hashing methods are MD5 and SHA1. Both can do a one way encryption like this: The MD5 of the string "abc" is: md5('abc') = 900150983cd24fb0d6963f7d28e17f72. The MD5 of "abc" will always be 900150983cd24fb0d6963f7d28e17f72. But if only one char changes, the MD5 will also change. md5('abd') = 4911e516e5aa21d327512e0c8b197616. So this function can create a hash out of a string which is "unique" for this string. But there is NO function like this: unmd5('4911e516e5aa21d327512e0c8b197616') = "abd" because it is a one way encryption. Like a one way flight, you cant get back home when you are at your destination.
You can play around a little bit with these function on:
This feature of a hash can be used to validate a password. So if Bob registers his new account, we do NOT save his password "donuts" in our database, we save md5('donuts') = 6c493f3632cf8c85348ebd89d1e3cafa. A attacker cant see directly what 6c493f3632cf8c85348ebd89d1e3cafa means. If Bob wants to login, we just have to check if md5($password_bob_entered) = 6c493f3632cf8c85348ebd89d1e3cafa. When it is true, Bob must have entered "donuts".
This way of saving password is the first step in the right direction, but still no good practice. The problem is, that a MD5 will always be the same for a specific string, this hole is used by several online pages and programs to reverse a md5. Just search for "md5 cracker" and you will find tons of ways for cracking MD5 or SHA1 hashes. This plus the fatal use of simple passwords like "donuts" (Google: "donuts") will make cracking only a matter of time. So once again, storing simple MD5 or SHA1 hashes is NOT secure.

MD5 / SHA1 hashes with salt

This technique is similar to the previous, with one difference, store something additionally called "salt". Like salt in a soup this extra string will make cracking password catchier. The procedure goes like this:
Bob registers with his password "donuts". We generate a random(!) salt, like "8ft$U3", and will hash his password like this: md5("donuts"."8ft$U3") = 9aaa94551048442a835f5ae875687714. Now we save his name, the salt and the salted password hash in our database. The clever ones of you might have noticed, we just made a secure password out of the simple "donuts". This technique is popular today and sometimes called "good practice". But this is dead wrong. In times of multicore CPUs and incredible fast graphic cards, breaking such a salted hash needs, if the password is not too complex, only a few hours. Current highend graphic cards are able to calculate billions of md5 per second, so breaking thousands of hashes is only a matter of time before a attacker can start his raid against all the cracked user account. So do NOT use simple salted hashes.

MD5 / SHA1 hashes with salt hashed multiple times

As we learned from the previous technique, cracking hashes is only a matter of time. So what takes cracking hashes longer? It is complexity. We can add these complexity by rehashing the hash again and again till we reached a level where cracking such a hash will take ages.
I wrote this PHP class for demonstration, you can use it wherever you want, but keep the copyright header:

PHP Secure Hash Class

The class is using everything what makes a hash complex enough to save it from being cracked. Salts are completely generated from special chars. Salts are random. Hashes are rehashed random multiple times. The whole hash is stored in a special format which can be saved easily in a database. The format looks like this: $method$salt$iterations$hash$.

Ideas for even more secure hashes

I wrote about storing hashes in a database. The common way nowadays for an attacker is to find a SQL injection to read out your database. This is a major hole in security of websites. The whole password verification data is stored inside a database. My idea is to add another vector to this data which is not saved inside the database. Something like a global salt for all password hashes. This way, a attacker needs access to the database and to the source of the config files.
This idea is also implemented in my secure_hash php class.

Pitfalls of password hashing

There a a few pitfalls you have to care about when using hashes.

First, the charset of the password string is important. You have to understand how a hashing function works. It is not just randomizing a string and producing some chars. A hashing function does NOT work with chars, it works with their byte interpretation. It is also possible to create a MD5 from a file. So if your website is using the LATIN charset, your passwords will be hashed with the LATIN byte interpretation of these chars. If you use UTF8, these hashes can be completely different! A example: the umlaut 'ä' is in LATIN "961fa22f61a56e19f3f5f8867901ac8cf5e6d11f" but in UTF8 is is "89d39ea0600180286a2f794d8b2c4d5b02450ed9". Keep this in mind if you want to change the charset of your site.

Check for system chars! If i say system chars, i mean things like blanks, tabs, newlines and carriage returns. For example the string 'a' will be this SHA1: "89d39ea0600180286a2f794d8b2c4d5b02450ed9", but the string 'a ' will be "9384a39d69d104db5d1db7ccceae2ce0cb6c01c2".
To reduce this risk use the trim() function to remove this chars from the passwords.

Other Algorithms than MD5 or SHA1 (Update)

Jani Hartikainen mentioned in his comment, that there are more algorithms than just md5 and sha1 and some of them are very slow compared to md5.
That is a good point, which has some up and downsides.

Using a uncommon algorithm brings more security, because a attacker might not know the algorithm or have a tool for cracking it. Also is a slower hashing routine a problem for the attacker.

There might be a problem with uncommon algorithms, if you want to move to an other hoster, he might not support your hashing routine.

If you want to use something else then MD5 or SHA1, just have a look at MHash or Hash PHP modules.

If you want to use your own hashing method, my PHP Secure Hash Class supports it. Just have a look at the example class "secure_hash_example".

Personally, i would not use something like whirlpool, tiger or haval, because they create dependencies which make the whole system more complex. I would let the simplicty win here.

I hope you learned how to store passwords secure and will make the Internet a safer place.

Related posts:


Ein Kommentar zu “PHP: Everything you need to know about secure password hashing”

  1. Jani Hartikainen 10. April 2011 um 21:55

    Very good advice here. I'd just like to mention one thing.

    You talk about how modern GPUs can calculate md5 hashes very fast. This is why you should use something else than md5 or sha1 - they are both designed to be fast.

    If you can, you could consider using blowfish instead, which is a much slower algorithm. It isn't slow enough to take too long when you just want to hash a user's new password or anything like that though, so it's usable in that regard.

    You can use Blowfish in PHP via PEAR Crypt_Blowfish or using the builtin crypt() function (altho not entirely sure if it has support for blowfish pre 5.3)