Using crypt()

When it comes to web-app authentication, cutting corners doesn’t buy you anything. It doesn’t save you coding time. It doesn’t give your users a better experience. All it does is weaken the security of your web site, needlessly putting your users, your employer, and yourself at risk.

Understanding Password Security.

(Disclaimer: Don’t blame me when your web app gets pwnd by Russian hackers.)

Proper password security has been discussed before, so unless I get really motivated one day, have a read of this material instead.

So now we know that simply storing a plaintext password, or a simple hash of a passsword isn’t enough! Preferably, we need a randomized salt, combined with an algorithm that is designed to be slow (or streching a fast algorithm).

The Solution.

If you haven’t already done so, I suggest you head over to the PHP manual page on crypt.
Now, when I first read that, I found that incredibly confusing, and I’m guessing I’m not alone (judging from the people I see getting confused with this function in ##php).

Now the reason for the function being implemented like that is to be consitent with Unix crypt, which makes sense. However, this means it’s harder for new users to use. But I digress, let us continue.

Before we start

Before we can jump right in, there are some version requirements. I highly recommend using the latest PHP 5.3, at least 5.3.2. The 5.2 branch is EOL.
Make sure you can follow along with the example, by checking if the CRYPT_SHA512 constant is both defined, and set to 1.

<?php

if (defined('CRYPT_SHA512') && CRYPT_SHA512) {
	echo "You can follow the example!";
} else {
	echo "Time to upgrade your PHP.";
}
?>

Note that you might be able to use crypt() even without SHA512. If you’re on 5.3.0 < 5.3.2, blowfish (bcrypt) is valid hash to use.

Generating the Hash.

<?php

// You would of course, get this from $_POST['Password'] or similar when registering an account, or changing a password.
$Password = 'MySuperSecretPassword123';

$HashedPassword = crypt($Password);
echo "We've generated a new hashed password of: {$HashedPassword}, from {$Password}.";

?>

See? Wasn’t that simple? Of course, it’s never that simple. If you read the manual, you should see the problem – An optional salt string to base the hashing on. If not provided, the behaviour is defined by the algorithm implementation and can lead to unexpected results.
You get no guarantees about anything. So, what do we do? We generate a hash where we tell PHP what’s going on!

For the purposes for this example, we’ll use the sha512 algo. You can use others, but realise that md5 is a poor choice, and sha1 is a poor choice for security critical applications.

Consider this example on generating a hash:

<?php

$Password = 'SuperSecurePassword123';

// These only work for CRYPT_SHA512, but it should give you an idea of how crypt() works.
$Salt = uniqid(); // Could use the second parameter to give it more entropy.
$Algo = '6'; // This is CRYPT_SHA512 as shown on http://php.net/crypt
$Rounds = '5000'; // The more, the more secure it is!

// This is the "salt" string we give to crypt().
$CryptSalt = '$' . $Algo . '$rounds=' . $Rounds . '$' . $Salt;

$HashedPassword = crypt($Password, $CryptSalt);
echo "Generated a hashed password: " . $HashedPassword . "\n";

?>

So, as you can see, we’re calling crypt() with two parameters, as described in the manual.
The first is simply the password (although I use $Password there, you would probably use $_POST[‘Password’], from when a user registers or changes password). The second is the “salt” described on the manual page, but I’ve split it up so you can see how each part of it “works”.

The salt follows the form “$ALGO$rounds=ROUNDS$SALT$” — It’s a mouthful alright.
The first part, “$ALGO$”. You can grab what you need for this on the PHP manual page for crypt(). We’re using sha512, so we use “$6$”. If you care to look at MD5, it states use “$1$”.
The next part, $Rounds, only applies to sha1 and sha512 algo’s (if we were using MD5, we wouldn’t include it). Basically, this tells PHP how many times to loop the hashing (this is the streching that was talked about in that article that you read). If you don’t understand why anyone would ever do this, read the first article I linked.
You should set this value as high as you can before you experience performance issues, or users start complaining. Quite simply, the higher this is, the more time it takes for someone to crack your passwords. Try 5000, increase in 1000 intervals, just play around.
Last, we have the salt. This can really be anything, as long as it’s from a large pool of possible values (meaning not just a number between 1 and 10).

Now, we combine all of this for some PHP Voodoo Magic™.

Running this script on Viper’s Codepad gives me:
$6$rounds=5000$4d2c68c2ef979$PZTAkwfvCZN0nT4La/0eNNKLt43w1B7DUkFNc9t1bnOG0OJRESnDa1E1H812TZ3CiBqd2qrcFrz2pk/kqpAy3/
which is the kind of output we expect.

If you instead get a password like: $6a7f58gxvDVU, it is likely that you’re not using PHP 5.3 or later, and that crypt() can’t use the sha512 algo. Upgrade your PHP.

So of course, now you just need to store this password in the database, and you’re ready to start authenticating logins! Or at least you will be when you read the next section.

Authenticating Users.

While the PHP manual on crypt() provides a fairly complete example for this, I find it still trips people up quite commonly.
To prove that it works fine, we’ll use that output we got from my above code (feel free to use your own if you would like).

<?php

$Password1 = 'WrongPassword';
$Password2 = 'SuperSecurePassword123';
$HashedPassword = '$6$rounds=5000$4d2c68c2ef979$PZTAkwfvCZN0nT4La/0eNNKLt43w1B7DUkFNc9t1bnOG0OJRESnDa1E1H812TZ3CiBqd2qrcFrz2pk/kqpAy3/';

// Now, what about checking if a password is the right password?
if (crypt($Password1, $HashedPassword) == $HashedPassword) {
	echo "Hashed Password matched Password1";
} else {
	echo "Hashed Password didn't match Password1";
}

if (crypt($Password2, $HashedPassword) == $HashedPassword) {
        echo "Hashed Password matched Password2";
} else {
        echo "Hashed Password didn't match Password2";
}

?>

Really! This time it is that simple!
If you’re wondering how crypt() does this, you need look no further than in front of you! Remember what crypt() does? It takes and algo, a rounds, and a salt, and uses them to get a hash for a password. If you said “5000 rounds, sha1, a salt of ‘delicious’ and a password of ‘lol’”, you would get the same result every time. So, our $HashedPassword has exactly that! The crypt() function will ignore the last bit, and see crypt($Password, '$6$rounds=5000$4d2c68c2ef979$');—you’re giving it the exact same salt that was used the first time! So it can consitently generate the same $HashedPassword string, if you give it the right password.

Wrapping up.

So, we’ve learned why not using a decent password system is silly, and what makes one good (well, other people wrote those articles, I just linked them), how to generate “secure” hashes, and how to authenticate a user using them.

This should be all you need to start using crypt() in your applications, but if you need any more help, come ask us at ##php on irc.freenode.net (and tell ss23 you read this article. If you’re having trouble understanding something, this article needs to be updated to help the next user who might have the same misunderstanding.)