skip to content

PHP: Better Password Encryption using Blowfish

This article explains how you can use Blowfish (a.k.a. bcrypt) hashing when storing passwords using PHP.

For details on why you should use Blowfish encryption instead of the standard crypt function you can read the links under References at the end of the article. It should already be clear that you never store passwords in the database as plain text.

The following examples assume that your PHP installation supports Blowfish encryption. You can test for this on your server using:

<?PHP if(defined("CRYPT_BLOWFISH") && CRYPT_BLOWFISH) { echo "CRYPT_BLOWFISH is enabled!"; } else { echo "CRYPT_BLOWFISH is NOT enabled!"; } ?>

If not, you should really think about upgrading to PHP 5.4 or the latest official release.

Using crypt() to store and check passwords

The crypt function is extremely easy to use. You generate or take from user input a new password and call crypt to generate a hashed version for storing in a database:

<?PHP $password_hash = crypt($password); ?>

Depending on your php version and configuration this will return a DES-based hash such as:

CrC3BP3gjcv8E

or, better, an MD5 hash:

$1$j9fuc/za$JCN3NPoTGjHvsAo6x7yDl1

The output will be different each time, but that doesn't matter because the 'salt' used to generate the hash is included at the start allowing it to be recreated.

You store this string ($password_hash) in the database alongside the username and other account details.

When the user tries to login you simply check whether the hash of the password they enter, using the database hash as a salt, re-creates (matches) the database hash:

<?PHP if(crypt($password_entered, $password_hash) == $password_hash) { // password is correct } ?>

The crypt function first identifies what flavour of encryption was used, extracts the salt, and uses that to generate a hash of the password the user input for comparison.

Change the hash type to Blowfish

The crypt function has a default hash type which in very old versions was DES, but now in most cases will be MD5. The beauty of the implementation is that it can recognise the hash type to use based on the format of the salt (found in the leading characters of the hash as described in the documentation).

To force crypt to use Blowfish hashing we need to pass a suitable salt when generating the database hash:

Blowfish hashing with a salt as follows: "$2a$", a two digit cost parameter, "$", and 22 digits from the alphabet "./0-9A-Za-z".

All we need to change then from the example above is to generate a suitable salt value.

Generating a random salt for Blowfish encryption

Here we have a simple function that creates a blowfish hash from the input value using a random salt made up of letters and numbers:

<?PHP // Original PHP code by Chirp Internet: www.chirpinternet.eu // Please acknowledge use of this code by including this header. function better_crypt($input, $rounds = 7) { $salt = ""; $salt_chars = array_merge(range('A','Z'), range('a','z'), range(0,9)); for($i=0; $i < 22; $i++) { $salt .= $salt_chars[array_rand($salt_chars)]; } return crypt($input, sprintf('$2a$%02d$', $rounds) . $salt); } ?>

Security advisory from PHP.net - developers targeting only PHP 5.3.7 and later should use "$2y$" in preference to "$2a$". Full details.

The default for the rounds variable has been set to 7 here, but depending on your system you might want to use a higher value. As you increase this value the time taken to generate, test, or try to brute-force the hash will increase significantly.

To generate a hash for a new password we call this new function in place of crypt:

<?PHP $password_hash = better_crypt($password); $password_hash = better_crypt($password, 10); // will take longer $password_hash = better_crypt($password, 15); // will take a LOT longer ?>

As computers get faster you will want to increase the cost (number of rounds), and for high security applications you can: increase the rounds; use a more random salt generator; or generate a hash using multiple hashing mechanisms in sequence.

The returned hash will now look something like this:

$2a$07$vY6x3F45HQSAiOs6N5wMuOwZQ7pUPoSUTBkU/DEF/YNQ2uOZflMIa

Make sure your database field is large enough. If the field size has been set to a fixed size for DES or MD5 then the Blowfish hashes will be truncated and unusable.

To test an entered password against the hash you can use exactly the same code as before, because the crypt function recognises that the password hash was generated using blowfish:

<?PHP if(crypt($password_entered, $password_hash) == $password_hash) { // password is correct } ?>

This approach will work even if your database contains a range of password types (DES, MD5, Blowfish, ...), which will be the case every time you change the algorithm used in your application.

Password encryption and verification in PHP 5.5

PHP 5.5 has a built-in function password_hash for generating password hashes, which as of now defaults to bcrypt (Blowfish), but that may change over time. You can also specify Blowfish explicitly.

Using this our better_crypt function can be replaced with:

<?PHP // Original PHP code by Chirp Internet: www.chirpinternet.eu // Please acknowledge use of this code by including this header. function better_crypt($input, $rounds = 10) { $crypt_options = [ 'cost' => $rounds ]; return password_hash($input, PASSWORD_BCRYPT, $crypt_options); } ?>

This will automatically use the new $2y$ salt format for increased security.

If you're feeling brave and don't mind which algorithm is used, you can let PHP use it's default settings (at time of writing, also bcrypt with a cost of 10):

<?PHP $password_hash = password_hash($new_password, PASSWORD_DEFAULT); ?>

For comparing the user entered password with the stored hash there is also a new function:

<PHP if(password_verify($password_entered, $password_hash)) { // password is correct } ?>

The password_verify function is designed to mitigate timing attacks and will work with other hash formats, not just Blowfish. It replaces the method above of having to apply crypt to the entered password ourselves for verification.

Putting it all together we have the following:

<?PHP $new_password = "<user input or randomly generated>"; $password_hash = better_crypt($new_password); // or password_hash($new_password, PASSWORD_DEFAULT); // => store in database ?> ... <?PHP $password_entered = "<user input at login>"; $password_hash = "<retrieved from database based on username>"; if(password_verify($password_entered, $password_hash)) { // password is correct } ?>

Remember that password_verify is only available in PHP 5.5 and higher. For earlier versions you need to use crypt and compare the strings yourself. Also the version of better_crypt where we generate the salt.

References

< PHP

User Comments

Post your comment or question

1 November, 2015

The above code for the shortened hash doesn't work. The correct code is:

$password_hash = password_hash($cleanpassword, PASSWORD_DEFAULT);

Updated now, thanks

18 September, 2014

if I understood, it doesn't need to store "salt" in DB , and to check the password, just need to take hashed password from DB and compare in
if(crypt($password_entered, $password_hash) == $password_hash){ ...

is it true?

The crypt function knows how to identify the salt and type of encryption based on the password hash.

  • if the hash is "CrC3BP3gjcv8E" then the salt is "Cr" and the encryption is DES.
  • if the hash is "$1$j9fuc/za$JCN3NPoTGjHvsAo6x7yDl1" then the salt is "$1$j9fuc/za$" and the encryption is MD5.
  • if the hash is "$2a$07$vY6x3F45HQSAiOs6N5wMuOwZQ7pUPoSUTBkU/DEF/YNQ2uOZflMIa" then the salt is "$2a$07$vY6x3F45HQSAiOs6N5wMuO" and the encryption is Blowfish.

13 April, 2014

Hey folks,

The thing to remember is that you never 'decrypt' the password when you check it. While some hash types are reversible, that's because they are not actually encryption.

Instead, you add the salt onto the clear text password, encrypt the new string and save this in the database.

Now when you want to check if a user logged in with the right password, instead of 'decrypting', you go add on the salt, encrypt the new string again and see if this is the exact result stored in your database.

Bcrypt is difficult enough to crack that, generally, even if someone stole you're whole database somehow - there's a good chance it wont matter. MD5 can be rainbow table'd, which makes it fairly easy to spot patterns, SHA512 and bcrypt, not so much.

9 March, 2014

Sorry, but I still do not understand how to compare the password a user enters to the value in the DB. First, as discussed above, I got a separate value when running better_crypt against the same value, as below ::

better_crypt('MyPassword') = $2y$07$tEbDSnMij51CEZlo8Ao2uerH­DmWhTi.1ip19rM/2MjAQmLnLSd5Pa
better_crypt('MyPassword') = $2y$07$bmnvU66lzGD9Yu0lDBEXb.FrtLcCkA/YJUXrPLK5iSNCdiyJXt7om

My understanding is that the "password" field in the DB can be either of these values ($2y%07%...5Pa or $2y%07%...7om). Is there then another field in my db table as the password hash, which above is "tEbDSnMij51CEZlo8Ao2ue" and "bmnvU66lzGD9Yu0lDBEXb.", which is used to check the password entered during the login?

3 March, 2014

Seems when one switches from plain text passwords to blowfish passwords, it is important to increase the size of the datafield. My passwords were getting truncated in the database. Took me about 45 minutes to figure that one out. :0

3 March, 2014

OK. Now I'm confused. The user creates an account with the better_crypt() function. But that creates a randomized salt. When the better_crypt() function is used from the login page, it encrypts a different hashed_password. To test, I created two accounts with the same password and they do not match in the database.

You don't use better_crypt() from the login page, but crypt(), passing the hashed password as the second parameter.

The crypt() function will then use the salt portion from the hashed password when generating a hash for the entered password.

27 October, 2013

I'm confused.

To test an entered password against the hash you say to use this code:

if(crypt($password_entered, $password_hash) == $password_hash)

Where is the database value that the crypt() result is being checked against? When a new user signs up, their login and blowfish-encrypted password is added to the table.

When they attempt to login the second time, I need to compare the crypt() with the value currently residing in the table, correct?

Excuse me if I have misunderstood something, but the $password_hash seems to be simply the encrypted form of the user's input twice - meaning whatever the user inputs as their password on login will be checked... against itself?

The value stored in the database is $password_hash. This is the user's blowfish-encrypted password. When they return and log in the password they enter is encrypted using the database value, a process that only uses the hash component.

If the output of this process is identical to the database value then you have confirmed that the password entered matches the original password.

3 July, 2013

we are encrypting particular data using crypt().....How to decrypt that data?

The crypt() function returns a 'hashed string' of a fixed length and not an encrypted version of your input. There is also no decrypt function, since crypt() uses a one-way algorithm.

ref: php.net/function.crypt.php

For two-way encryption in PHP you can use the Mcrypt or OpenSSL cryptography extensions.

ref: php.net/refs.crypto.php

28 May, 2013

maybe I've misunderstood something

if(crypt($password_entered, $password_hash) == $password_hash)

What exactly is stored in these variables? Is the first $password_hash, just the hashed password that is stored in the database? Or is it meant to be stored salt?

It never seems to match

The password_hash is the first part of the encrypted password - highlighted in the examples above. It is typically the first two characters (DES) or longer and starting with $x$ where x is a number defining the encryption method.

But rather than separating out the hash you can simply pass the entire encrypted password and crypt() will use the part it needs.

21 February, 2013

This seems unpredictable and wrong. When I try it on a string like "password" sometimes it outputs a long hashed up string and sometimes it outputs "*0" This makes no sense. And how can you ever check against it if it always produces a different string?

We've never seen that behaviour and this function is being used a lot on our servers. Are you sure you have the code copied correctly, and your server supports CRYPT_BLOWFISH?

15 February, 2013

@JOHN LODGE - You make a good point, however, you need to think about this from an insider threat perspective as well. If I'm using a tool like SQL Workbench and or SQL Developer, for example, and I am running queries on my "login" table then without encrypted password a select statement on that table could yield something like:

Username Password .... etc.
johndoe password

If I'm using blowfish or another algorithm for encrypting the password then I see something like:

Username Password .... etc.
johndoe $2a$0BkU/DEF/YNQ2uOZflMIa...

So if I run this query then get up and walk to the printer and "John Malicious Insider" walks past my desk he can see logins to my webapp. He does not have login information to the database but if the password is unencrypted, he does have logins to my application which can be just as detrimental.

21 January, 2013

Just switched from using sha1 + salt to blowfish, thanks! also helpful for dos attacks

3 January, 2013

I have always had this misunderstanding regarding the point of encrypting passwords in the database.

Obviously the point of a password in the first place, is to prevent unauthorised users from gaining access to information stored in the database.

My issue is this. If someone gets as far as finding out that you have encrypted passwords, then they have already gained access to the database and all the data it contains, so breaking the password would seem fairly irrelevant at this point. It would seem to me that the only password that is important is the one to access the database in the first place, not a password stored within it.

Please enlighten me if I am missing a key point here.

As recent events have shown (LinkedIn, Yahoo!, Twitter and Last.fm for starters) you can't assume that your private data is always going to remain private. Your server and database could be hacked, or someone could 'find' your backups.

The next problem is that most people use the same or similar passwords everywhere they go. As soon as a list of passwords is exposed various groups will try them out on Facebook, Twitter, GMail, HotMail and other services to see if they can get access and build up a profile.

On our servers we use blowfish to encrypt all passwords in the database and our backups are further encrypted with GPG encryption.

9 December, 2012

I was reading about this type of encryption, but couldn't find something that explains about its functions and how it works.

This post helped me a lot! Thanks!

top