Upgrade To Better Passwords in PHP
The examples here assume that you currently have either unsalted or all-with-the-same-salt passwords stored in your database, hashed with md5 or sha1 or something. This is a Very Bad Idea (TM) since it’s trivial to recover unsalted passwords and not all that hard to figure out same-salted ones.
Enter the password_hash()
and password_verify()
functions which were included by default in PHP 5.5 but are also available for PHP 5.3.9+ via a userland implementation (see https://github.com/ircmaxell/password_compat). All new applications should use this but converting your existing application can be tricky so here’s my process.
How to Convert To Using PHP’s Password Features
Essentially, we’re going to re-hash the existing password database. We don’t have everyone’s plaintext passwords, so we’ll have a properly-hashed value containing a hashed value … we’ll handle this in code and clean up after ourselves later. I like this approach because it means you’re immediately protected rather than only being able to update passwords when people log in.
Update Login Code
Before we do anything to the password database, we’ll update the login code so that we’re still able to log in later! If you’re using an md5 hash then you probably have an SQL query that selects users with matching usernames and passwords, something like:
SELECT * FROM users u WHERE u.username = :username AND u.password = :password
This doesn’t work with the new password features because the algorithm and salt used are part of the stored value; we will need to get the hashed value, then ask PHP to work out if it matches the password that the user supplied. So the first thing to do here is to amend the code so that we:
- Fetch users with a matching username, including the hashed password value that is stored in the database
- Pass the supplied password value from the form along with the stored hash to
password_verify()
to find out if the passwords matched - If they don’t match, then hash the supplied password value with the old password algorithm (and salt if used), and try that instead!
This will allow us to hash the password database, and be able to log in.
Hash Existing Passwords
To do this, we will need to loop through the MySQL table and update all the values. I do this with a one-off PHP script and work through updating one row at a time, since I need to calculate the new hashed value using PHP’s password_hash()
function.
At this point, we’ve got a lot of values flying around, but they’re visually distinct so let me recap:
- There’s the original user password e.g.
qwerty
, this wasn’t ever stored anywhere - There’s the old stored password, if you use
md5()
then it would bed8578edf8458ce06fbc5bb76a58c5ca4
- There’s the new hash of the password. This has quite a different format and will look something like:
$2y$10$qxnaCfygnb/z6bpqPf/S4e3DsjKZ6bqcR3tsWLwc4zcJ3l.tVA2La
. It’s longer because it has the algorithm and salt included in it
OK so: database now holds new format password hashes BUT they are hashes of hashes, which is more overhead than we need, so let’s fix that.
Update Registration Code
Make sure that newly-registered users get their password stored in the new format when they pick a password. Our existing changes to the login code should then allow them to log in.
Usually I find that there are also other places that need changing, such as when a user changes or forgets their password – grep your code for whatever the name of the password column in the database is to find them all!
Stored Hashed-Once Passwords On Login
Now, when a user logs in, if they have a password that has been hashed once, we’ll accept their login. Then as we set up in our first step, if they fail that check, we have a second attempt to log them in, by hashing their password to match our old algorithm, and trying that. This allows the existing users whose passwords we then doubly-hashed to log in, but it’s a lot of processing just to log in a user, so we’ll amend the login code to add one final step:
- While we have the user’s plaintext password, hash it properly and store it – so next time they can log straight in
Over time, users will eventually get their passwords into the proper format when they log in – you could also add a column to indicate what format password they have or whether it’s been properly rehashed if you want to know who has what.
Alternative Approaches
There certainly are alternatives, most of them involve changing user’s passwords when they log in, and/or forcing all users to go through a password change on their next login. An elegant solution I saw recently is in the CakePHP3 migration guide. This allows the definition of two password handlers: a real one, and a fallback. The real one will be used – but if password verification fails then the fallback one will be tried as well. This allows for easy migration to a better password strategy and works particularly well since the older versions of Cake did use a single salt plus SHA-1, so I was very happy to see that they had really thought about how users could upgrade, and made it very easy both technically and by including it in their migration guide.
The password_hash function is not available on my server because the PHP version is still 5.4. Perhaps you can suggest alternative methods for those of us that are still using old versions of PHP (which is quite a large part of the PHP user base).
Ah, sorry, I totally should have mentioned that there is a library you can use for versions of PHP >=5.3.9 but still < 5.5. You can find that here and just include it in your projects https://github.com/ircmaxell/password_compat
I will go check that out, thanks :)
This is a good library for making using the password_* functions even easier. It also supports legacy hash conversion:
https://github.com/jeremykendall/password-validator
Ah, hadn’t seen that, thanks Gareth (and thanks Jeremy too!)
I think the SQL should be syntax highlighted rather than displaying “pygment” tags in square brackets?
Thanks for mentioning that, I’ve been having some issues with my syntax highlight plugin but I think I’ve really fixed it this time (famous last words…)
Yes, now it’s working. Too bad it’s probably not possible to add a few tests to make sure it works to avoid it being famous last words :D
I work with PHP Password Library
Is a simple, easy to use password hashing library for PHP 5.3+. Several password hashing schemes are supported by the library, including bcrypt and PBKDF2. The project is inspired by Python’s Passlib.
http://rchouinard.github.io/phpass/