Please Stop: Securing User Accounts with mySQL's Password() Function...

I tweeted a week or so ago:  Please stop using mySQL's password() function.  I had good reason for doing so - the legacy software I've been assigned to maintain until I can help with the new rev has a password schema that is based entirely on mySQL's password function as the hash strategy for storing user-account passwords in the database. As developers, we should be getting that uncomfortable, squishy, feeling whenever we read about another corporate hack event, one that exposed gazillions of user accounts to the ether, and zomg, we even got their passwords!!!

There are really only two, well three, ways to store user account passwords within a software system.  You can store them as clear-text, (legitimate depending on your system design and needs), encrypted, or hashed.

Most of us LAMP developers choose the hash method - it's easier since the PHP function uses the same algorithm as the mySQL password function and, bonus, we also have "plausible deniability" when it comes to risk-exposure because hashing is a one-way encryption -- we can't tell you what your password is because we don't know.

Encrypting a password takes a couple more steps in the code to accomplish, but the bonus here is that you can reverse the encryption and generate the user's original password from the scramble.  It poses only a lower "comfort" level for business rules since it can be "cracked" eliminating the soft comfy blanket "plausible deniability" brought you by hashing.

Hashed passwords can be cracked.  If you're using mySQL's password function, then they can be hacked, too.

What?!?

If your database ever exposes your user's passwords, all a hacker would see is a 41-byte string (assuming mySQL 4.1 or later)  of random characters.  However, the algorithm used to generate this 41-byte string is fairly well known.  I googled the process last week and I found API's written in everything from Javascript to PHP that provide the same functionality - taking a cleartext password and applying the algorithm to generate the 41-byte hashed string.

This is how we, as developers, validate users logging into our systems.  We obtain the user's clear-text password using an HTML form and, more often than not, we just shoot that clear-text over the wire from the client machine to the server, where the server uses an on-the-fly hashing to compare the value against the stored value in the db:

[cc lang='php' ]

if (md5($userPassword) == $dbResult['password']) {

[/cc]

Since we're comparing hash-values and not passwords, we somehow feel safer and more secure, right?  I mean, we're not really dealing with clear text, so it must be good...

The PHP function md5 is a fast hashing algorithm.  The PHP website itself recommends NOT using md5 as a method of securing passwords because of this very reason.  Hackers will use a brute-force approach to cracking your hashed password value - usually against a dictionary specifically collated for this purpose - and can break your hash in a matter of seconds.

Consider if I've obtained the password hash for a user account, all I need to do is to run my password cracker, with it's huge dictionary of common passwords, through a simple md5 algorithm, and compare the output value against the hash I hacked off your system.  I guarantee you, for most user accounts, I'll get the 'ding - the turkey's done!' chime within seconds.

Good user-account security takes effort.  Not a lot of effort; we're only talking about a couple more lines of code, here.  But some effort.  What I'm proposing isn't going to turn your user-authentication design into an electronic Ft. Knox.  But it will make your account harder to crack than some other poor sucker's website.

5 Steps to Better Password Security:

1. Stop sending passwords in cleartext over the wire.  

If you can't use an SSL connection back to your server, then at least encrypt the password client side using Javascript and return the encrypted value back with your form submit.  Something is better nothing in this case.  If you're sending your passwords clear-text from the login form back to the server, (and you'd be amazed at how many websites do this!), then you're begging to be hacked.

2. crypt() is your friend

crypt() is a PHP function based on the mcrypt library and is part of the current PHP core.  So there's no good reason for you to not use it.  Others will argue for the hash() function because it offers more encryption algorithms than does crypt().  While the hash() extension is bundled with PHP, it has to be added at compile time -- no guarantees that hash() will be available to you.

3. Pick a good salt

The Salt is the second parameter to the crypt function and is applied during the hash to prevent your hash from appearing in a rainbow table of pre-computed hashes.  Don't store your salt in your database tables either.  Store it outside of directory root.  I like to use long sentences for my salts that are easy to remember (for me) but make little sense to anyone else.  ('My ex-wife used to make lemon-flavored chicken that tasted like hammered owl shit.')

4. crypt(Blowfish > SHA1 > md5) > md5() = Password()

If you're going to go to the trouble of using the crypt library to secure your user passwords, then at least use the Blowfish algorithm.  According to the documentation, it's computationally more intensive than SHA1 or MD5 but it scales.  For extreme paranoia, consider using SHA256 or SHA512.  If you've got the room to store these passwords, and the CPU cycles to process, then they're your best bet using this schema.

5. Hash the hash

There's nothing that prevents you from hashing the hash.  First pass, process the password using SHA1 and pass that hash back into crypt using Blowfish.  Two or three-pass encryption is common enough and adds an extra step of complexity that makes your system less desirable to hackers.

 

Remember, crypt is still a one-way hashing algorithm just like the mySQL password().  Earlier in the article I mentioned encrypting passwords as a means of securing them.  In this context, encrypting a password (as opposed to hashing as password) implies decryption -- that you'll be able to reconstruct the original password from the encrypted output.

That, friends, is a blog post for another day.