public
Last active

Simple bcrypt object to wrap crypt() with.

  • 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
<?php
 
// Originally by Andrew Moore
// Src: http://stackoverflow.com/questions/4795385/how-do-you-use-bcrypt-for-hashing-passwords-in-php/6337021#6337021
//
// Heavily modified by Robert Kosek, from data at php.net/crypt
 
class Bcrypt {
private $rounds;
private $prefix;
private $salt_prefix;
public function __construct($prefix = '', $rounds = 12) {
if(CRYPT_BLOWFISH != 1) {
throw new Exception("bcrypt not supported in this installation. See http://php.net/crypt");
}
 
$this->rounds = $rounds;
$this->prefix = $prefix;
// Determine if this version of PHP has the Bcrypt fix
$this->salt_prefix = version_compare(PHP_VERSION, '5.3.7') >= 0 ? '$2y' : '$2a';
}
 
public function hash($input) {
$hash = crypt($input, $this->getSalt());
 
if(strlen($hash) > 13)
return $hash;
 
return false;
}
 
public function verify($input, $existingHash) {
$hash = crypt($input, $existingHash);
 
return $hash === $existingHash;
}
 
private function getSalt() {
// the base64 function uses +'s and ending ='s; translate the first, and cut out the latter
return sprintf('%s$%02d$%s', $this->salt_prefix, $this->rounds, substr(strtr(base64_encode($this->getBytes()), '+', '.'), 0, 22));
}
private function getBytes() {
$bytes = '';
 
if(function_exists('openssl_random_pseudo_bytes') &&
(strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN')) { // OpenSSL slow on Win
$bytes = openssl_random_pseudo_bytes(18);
}
 
if($bytes === '' && is_readable('/dev/urandom') &&
($hRand = @fopen('/dev/urandom', 'rb')) !== FALSE) {
$bytes = fread($hRand, 18);
fclose($hRand);
}
if($bytes === '') {
$key = uniqid($this->prefix, true);
// 12 rounds of HMAC must be reproduced / created verbatim, no known shortcuts.
// Changed the hash algorithm from salsa20, which has been removed from PHP 5.4.
for($i = 0; $i < 12; $i++) {
$bytes = hash_hmac('snefru256', microtime() . $bytes, $key, true);
usleep(10);
}
}
return $bytes;
}
}
 
?>

What is the basis of the statement "12 rounds of HMAC must be reproduced / created verbatim, no known shortcuts."? No matter how many rounds you do it for, it always produces a 64 byte string. Why not just do it once? Or why not just use uniqid() as the salt if the other random generators are unavailable and forget about hashing it?

HMAC requires not only a password but a different key initialization scheme. Normal hashes like the example below can be used as "partial hashes" by saving their states, reducing time to calculate derivative hashes. This is why prepending a salt to a non-HMAC hash is foolish!

sha1( 'the quick brown fox' )
sha1( 'the quick brown fox jumped' )
sha1( 'the quick brown fox jumped over' )

A clever attacker could use the prior hashes to make each successive hash take only moments. HMAC

For an attacker to programmatically derive our resulting hash they must be able to know/predict the result of uniqid and must also know the key used to encipher the hash. If they only compromise the database and know we've used this technique their only chance is to brute-force the resulting hash.

For an attacker to brute-force our resulting hash they need to know what is hashed in the final iteration, and what the key is. Naturally, if they compromise your filesystem too this is already known. However, even then they must know the penultimate hash plus the result of the microtime call!

Then all they know are the random bytes used as a salt for the BCrypt hash... but then, they know the first ~11 characters of it anyway.

I'm a bit confused about what you're saying here, especially since there are multiple hashes being calculated and you don't define which one you're talking about. My point is that the result of hash_hmac() is not used to hash anything sensitive at all. The result of that function is It is only being used as a salt and is stored in plain text. Even if a malicious hacker were to somehow determine the original uniqid() or key or microtime() used with hash_hmac(), this gives them absolutely no further information in order to determine the result of the password hashing with crypt(), which is why I cannot see any benefit of using some complicated "random" hash over simply using uniqid() as a salt for crypt().

My previous response was discussing the derivation of the final hash.

What you're saying is correct though, only 1 iteration is strictly required because they will know the first 11 bytes of the result anyway. And that's all that matters for bcrypt.

I've updated this gist by switching from salsa20 to snefru256 as the fallback method for generating random bytes.

I've updated the gist to check for the PHP 5.3.7 or higher, and use the fixed bcrypt implementation.

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.