<?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$'; } |
This comment has been minimized.
This comment has been minimized.
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. |
This comment has been minimized.
This comment has been minimized.
Hmmm, looks nice and simple... would be good in a proper repo, though. |
This comment has been minimized.
This comment has been minimized.
Seems like you shouldn't have to check for php version 5.3, just need to check for |
This comment has been minimized.
This comment has been minimized.
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 ) ? |
This comment has been minimized.
This comment has been minimized.
@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. |
This comment has been minimized.
This comment has been minimized.
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. |
This comment has been minimized.
This comment has been minimized.
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. |
This comment has been minimized.
This comment has been minimized.
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. |
This comment has been minimized.
This comment has been minimized.
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):
|
This comment has been minimized.
This comment has been minimized.
So, if I got it right: the hashing method always generates different hashes, but the check method uses the hash itself as salt? |
This comment has been minimized.
This comment has been minimized.
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. |
This comment has been minimized.
This comment has been minimized.
That's nice, thanks for the explanation. I'm saying this from a security newbie perspective... |
This comment has been minimized.
This comment has been minimized.
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. Makes sense now, thanks! |
This comment has been minimized.
This comment has been minimized.
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 < ***@***.*** > 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."
|
This comment has been minimized.
This comment has been minimized.
Never mind, I was being massively thick. Ignore me. |
This comment has been minimized.
This comment has been minimized.
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? |
This comment has been minimized.
This comment has been minimized.
@timk95 this is what confused me. The salt is stored as the first part of the hash. The |
This comment has been minimized.
This comment has been minimized.
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.
|
This comment has been minimized.
This comment has been minimized.
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? |
This comment has been minimized.
This comment has been minimized.
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.
|
This comment has been minimized.
This comment has been minimized.
Thanks for the information. |
This comment has been minimized.
This comment has been minimized.
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: 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 An additional gotcha: OpenSSL's random_pseudo_bytes function can be slow. |
This comment has been minimized.
This comment has been minimized.
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.
Thank you for your article. it has been helpful in my understanding the issues involved. |
This comment has been minimized.
This comment has been minimized.
@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. |
This comment has been minimized.
This comment has been minimized.
Why the hell is there an OpenSSL-thing in this code ? OpenSSL is not activated by default on PHP 5.3+. |
This comment has been minimized.
This comment has been minimized.
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! |
This comment has been minimized.
This comment has been minimized.
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) |
This comment has been minimized.
This comment has been minimized.
Rather than using @coastwise has a fork featuring such a comparison. It would be cool if @marcoarment could implement that here. |
This comment has been minimized.
Removed references to phpass — pretty much all traces of its code have since been removed.