public
Last active

Simple PHP 5.3+ Bcrypt class and functions

  • Download Gist
Bcrypt.php
PHP
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103
<?php
/*
By Marco Arment <me@marco.org>.
This code is released in the public domain.
 
THERE IS ABSOLUTELY NO WARRANTY.
 
Usage example:
 
// In a registration or password-change form:
$hash_for_user = Bcrypt::hash($_POST['password']);
 
// In a login form:
$is_correct = Bcrypt::check($_POST['password'], $stored_hash_for_user);
 
// In a login form when migrating entries gradually from a legacy SHA-1 hash:
$is_correct = Bcrypt::check(
$_POST['password'],
$stored_hash_for_user,
function($password, $hash) { return $hash == sha1($password); }
);
if ($is_correct && Bcrypt::is_legacy_hash($stored_hash_for_user)) {
$user->store_new_hash(Bcrypt::hash($_POST['password']));
}
 
*/
 
class Bcrypt
{
const DEFAULT_WORK_FACTOR = 8;
 
public static function hash($password, $work_factor = 0)
{
if (version_compare(PHP_VERSION, '5.3') < 0) throw new Exception('Bcrypt requires PHP 5.3 or above');
 
if (! function_exists('openssl_random_pseudo_bytes')) {
throw new Exception('Bcrypt requires openssl PHP extension');
}
 
if ($work_factor < 4 || $work_factor > 31) $work_factor = self::DEFAULT_WORK_FACTOR;
$salt =
'$2a$' . str_pad($work_factor, 2, '0', STR_PAD_LEFT) . '$' .
substr(
strtr(base64_encode(openssl_random_pseudo_bytes(16)), '+', '.'),
0, 22
)
;
return crypt($password, $salt);
}
 
public static function check($password, $stored_hash, $legacy_handler = NULL)
{
if (version_compare(PHP_VERSION, '5.3') < 0) throw new Exception('Bcrypt requires PHP 5.3 or above');
 
if (self::is_legacy_hash($stored_hash)) {
if ($legacy_handler) return call_user_func($legacy_handler, $password, $stored_hash);
else throw new Exception('Unsupported hash format');
}
 
return crypt($password, $stored_hash) == $stored_hash;
}
 
public static function is_legacy_hash($hash) { return substr($hash, 0, 4) != '$2a$'; }
}
 
 
// =============================================================================
// Or, if you don't want the class structure and just want standalone functions:
// =============================================================================
 
function bcrypt_hash($password, $work_factor = 8)
{
if (version_compare(PHP_VERSION, '5.3') < 0) throw new Exception('Bcrypt requires PHP 5.3 or above');
 
if (! function_exists('openssl_random_pseudo_bytes')) {
throw new Exception('Bcrypt requires openssl PHP extension');
}
 
if ($work_factor < 4 || $work_factor > 31) $work_factor = 8;
$salt =
'$2a$' . str_pad($work_factor, 2, '0', STR_PAD_LEFT) . '$' .
substr(
strtr(base64_encode(openssl_random_pseudo_bytes(16)), '+', '.'),
0, 22
)
;
return crypt($password, $salt);
}
 
function bcrypt_check($password, $stored_hash, $legacy_handler = NULL)
{
if (version_compare(PHP_VERSION, '5.3') < 0) throw new Exception('Bcrypt requires PHP 5.3 or above');
 
if (bcrypt_is_legacy_hash($stored_hash)) {
if ($legacy_handler) return call_user_func($legacy_handler, $password, $stored_hash);
else throw new Exception('Unsupported hash format');
}
 
return crypt($password, $stored_hash) == $stored_hash;
}
 
function bcrypt_is_legacy_hash($hash) { return substr($hash, 0, 4) != '$2a$'; }

Removed references to phpass — pretty much all traces of its code have since been removed.

Heard you mention bcrypt in B&A recently and decided to change my hashes. I'm a security guy by trade and it's really great to see developers spreading awareness about security practices. Thanks for sharing, Marco. Love your work! And for anyone that's still using md5 or sha1, please consider making the switch. Marco has made it so simple there's hardly any excuse not to.

Hmmm, looks nice and simple... would be good in a proper repo, though.

Seems like you shouldn't have to check for php version 5.3, just need to check for CRYPT_BLOWFISH == 1.

Hi there,

Is this a PHP implement for Bcrypt ( http://en.wikipedia.org/wiki/Bcrypt ) or for Crypt ( http://en.wikipedia.org/wiki/Crypt_%28Unix%29 ) ?
Thanks.

@Jonadabe: Neither, technically. It's not an implementation of bcrypt, it is wrapper functions to simplify the use of bcrypt in php versions greater than or equal to 5.3. Hashing in php can use the php crypt() function, which in turns tries to use the unix crypt command line function if it can, but in php greater than or equal to 5.3, if unix doesn't provide certain hashes, php picks up the slack and uses a php native implementation of them, so it won't matter what hashes the server has implemented for command-line crypt, you'll have access to bcrypt regardless!

And since the wrapper simply throws an exception if you're on a php below 5.3, so you'll either have bcrypt, or the function wrappers will throw you an exception error message.

Alltogether, it's perfect, because you're not relying on some random stranger to implement bcrypt, you're replying on the php developers, but these wrappers are great because they make it much simpler to get it right, as you can see above.

I like them.

When I use this, every time I reload, the bcrypt hash changes. Therefore, Bcrypt->check() never returns true. I've modified it slightly, but it seems like it should work.

Even if I just call Bcrypt->hash('test') the hash changes with every page load.

May want to check/update your php version, there've been problems with the php bcrypt implementation discovered. Also, be sure that you're using the right format for putting into into the bcrypt_check, since you'll need an existing hash to compare against anyway.

Thanks for the super fast reply. As I kind of expected, the hash does change on each regeneration, and I was able to get it working, although, I'm not entirely certain what I did other than letting the code handle it rather than manually testing it.

Here's an alternative function to calculate the random key for the salt for environments where openSSL cannot be enabled for whatever reason (do note however that openssl_random_pseudo_bytes will always be the superior option):

    //Usage: openssl_random_pseudo_bytes(16) -> pseudoRandomKey(16)

    function pseudoRandomKey($size) {
            if (function_exists('openssl_random_pseudo_bytes')) {
                $rnd = openssl_random_pseudo_bytes($size, $strong);
                if($strong === TRUE) return $rnd;
            }

            $sha=''; $rnd='';
            for ($i=0;$i<$size;$i++) {
                $sha = hash('sha256',$sha.mt_rand());
                $char = mt_rand(0,62);
                $rnd .= chr(hexdec($sha[$char].$sha[$char+1]));
            }

            return $rnd;
    }

So, if I got it right: the hashing method always generates different hashes, but the check method uses the hash itself as salt?
but isn't it unsafe to keep the salt (the hash itself) "exposed" in case of database break-in?

Well, let's see:

You ideally want to use a -random- salt for every record. Really good random salts are built into this hash generation for bcrypt. Unlike a global salt, since the salt exists for each bit of information and is random, you can't simply store it in the source, you pretty much need a database anyway. And putting it anywhere other than with the hash is problematic because if that info gets unlinked, you no longer have the password, you have unretrievable, unmatchable trash. So the gain from separating the hash and the salt is low, the potential pain is extremely high, so you might as well have them be integrated together.

That's nice, thanks for the explanation.
But potentially somebody with db access and rainbow tables could still discover weak passwords, right?

I'm saying this from a security newbie perspective...

I think I got it: one would have to re-hash each hash/salt with a large table of clear passwords and then compare against it.
And with the work factor, each hashing operation could be longer and heavier.

Makes sense now, thanks!

Yeah, precomputation/rainbow tables isn't effective against a hash that is
randomly salted per password.

I don't actually know why an application global salt wouldn't be an
improvement (by separating part of the system off of the database), I'm
going to look into that. My expectation is that it could go either way
because it's a common thread between otherwise nicely randomized password
results. But it's something to look into.

On Mon, Apr 23, 2012 at 8:22 AM, David Gasperoni <
reply@reply.github.com

wrote:

I think I got it: one would have to re-hash each hash/salt with a
large table of clear passwords and then compare against it.
And with the work factor, each hashing operation could be longer and
heavier.

Makes sense now, thanks!


Reply to this email directly or view it on GitHub:
https://gist.github.com/1053158

+++++++++++++++++++++++++++
BitLucid.com http://bitlucid.com/ - BitLucid, Inc. - Web & Programming
Consultants
ninjawars.net - My webgame project
github.com/tchalvak/ninjawars - My open source projects
dnaexmosn.deviantart.com - Art Gallery
585-519-7658 - Cell
585-502-7658 - Office Number
"Never compare your inside with somebody's outside."

Never mind, I was being massively thick. Ignore me.

I'm confused, in order to check the password you need to store the salt isn't it? And the hash changes on every reload but when i check the hash with the plain password it returns true how does PHP know wich salt got used since it's rondom every time?

@timk95 this is what confused me. The salt is stored as the first part of the hash. The $work_factor that you supply, I'm assuming, is used by crypt() to determine what part of that whole string is the hash when you go to compare the password with the hash in future. Either that, or it's much cleverer than that, but what I do is store the everything returned by hash() then pass that as the $stored_hash parameter to check() along with the password supplied by the user for authentication as $password. Either that, or crypt is much smarter than I assume, but the last point still stands.

You store the salt and the hash in one field.
Even with salt and hash potentially exposed, one would need to brute force each single record because the salt is different each time.
When you authenticate the password, you just feed the record (saved salt + hash) to the crypt function with the submitted password.

To increase security, you can raise the work factor. This will slow down the hashing, making brute force attempts harder and slower.

Ah thanks, that explains allot. Can you just change the work factor in the hash or does that need every password to be hashed again?

Well, it is saved with each password, so as far as I understand it, if you change it, it will affect hashes from this point on. Old ones have the former work factor built into them, and so it gets read by the auth check.

Simply put, people would need to change their passwords to make them more secure.

Thanks for the information.

To clarify anyone's confusion about the crypt function: The PHP manual is quite confusing, as crypt()'s second argument isn't strictly a salt. It's a magically formatted string that contains an algorithm identifier, a work factor for bcrypt, optionally a salt, and optionally a finished hash. The first hash() function in this gist generates a salt with that special format; the '$2a$' bit identifies that crypt() should use bcrypt, then adds a padded-to-two-characters work factor, and then adds a salt. Crypt then takes this string and creates a completed hash.

A completed hash looks like this:
$2a$13$MPSSp1VV8l9T9ZWSWH02teV2BLdHkRZpANht3F2RT0/HStLuNS752
That tells crypt() that it uses bcrypt, has a work factor of 13, and includes both a salt (22 characters long) and the hash. Just a salt would be much shorter. This is what you want to store in your database to match against future passwords.

The check() function then passes the fully finished hash, which is also prefixed by '$2a$', the load factor, and the salt, to crypt(). Crypt identifies that we're passing it a full hash, not just a salt, so it matches the provided password with the hash. To indicate a match, crypt() returns the exact same hash.

It's pretty confusing behavior, especially to those used to the typical $hash = sha1($salt . $password) and if($hash == sha1($salt . $password)) model. This is the entire reason why this gist was valuable to me and to others; it simplifies this unnecessarily confusing behavior into something practical.

An additional gotcha: OpenSSL's random_pseudo_bytes function can be slow.

After some research on the web regarding security of my web site I have decided to use your bcrypt script to store passwords. I do have some question regarding bcrypt which I hope you could answer.

  1. I understand that I won't need a salt column in my database because a salt is randomly generated and stored as part of the hash. So about 30 characters of the hash are reserved for salts. This must reduce the amount of characters left for the encrypted part of the hash. Does this increase the amount of collisions that would occur? i.e. a password attempt that should not be successful but resolves to a match. Can the length of the output hash be increased to reduce collisions? Is it necessary to do so?

  2. I understand the point of using bhash is computing power. If someone reads the hash it would require a great deal of 'their' computing power to derive the password (or suitable collision). This does not necessarily protect against someone using a brute force attack against 'my' server trying common passwords. So having a log in system which limits unsuccessful attempts would still be prudent.

Thank you for your article. it has been helpful in my understanding the issues involved.
:-)

@decahedron: bcrypt doesn't natively allow you to generate a longer hash, and it's typically not necessary. Its primary defense against both collisions and brute forcing is in its slowness and difficulty in calculating. So looking for collisions would still be a challenge.

And naturally, if bcrypt is running on your server, leaving the login system without a rate-limiting mechanism actually means attackers can try to overload your server by means of a DOS attack. This is partially why rate-limiting is so critical in such infrastructure.

Why the hell is there an OpenSSL-thing in this code ? OpenSSL is not activated by default on PHP 5.3+.

There's also ircmaxell's php 5.5 compatible version that works seamlessly as a drop in replacement for the new bcrypt hashing interface in PHP 5.5. Enjoy!

I think there should be "===" operator instead of "==" in bcrypt_check method/function.

I'm not sure about that bcrypt can return numeric hash, but PHP < 5.4.4 have some problems with comparing strings if they are numeric (http://phpsadness.com/sad/47)

Rather than using == (or ===) for the check function you should use a comparison function that takes a constant time, to combat against timing attacks.

@coastwise has a fork featuring such a comparison. It would be cool if @marcoarment could implement that here.

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.