开发者

How can we create a fairly secure password hash in PHP?

开发者 https://www.devze.com 2023-03-13 00:28 出处:网络
I have been reading up on password hashing, but all the forums I read are full of posts from people debating theory behind it that I don\'t really understand.

I have been reading up on password hashing, but all the forums I read are full of posts from people debating theory behind it that I don't really understand.

I have an old (and presumably extremely weak) password script that reads like this: $hash = sha1($pass1);

function createSalt()
{
$string = md5(uniqid(rand(), true));
return substr($string, 0, 3);
}

$salt = createSalt();
$hash = sha1($salt . $hash);

If I understand correctly, the longer the salt, the larger the table the hacker has to generate in order to break the hash. Please correct me if I am wrong.

I am looking to write a new script that is more secure, and I am thinking that something like this would be okay:

function createSalt()
{
$string = hash('sha256', uniqid(rand(), true));
return $string;
}


$hash = hash('sha256', $password);
$salt = createSalt();
$secret_server_hash =     'ac1d81c5f99fdfc6758f21010be4c673878079fdc8f144394030687374f185ad';
$salt2 = hash('sha256', $salt);
$hash = $salt2 . $hash . $secret_server_hash;
$hash = hash('sha512', $hash );

Is this more secure? Does this have a noticeable amount of overhead?

Most importantly开发者_StackOverflow, is there some better way to make sure that the passwords in my database cannot be (realistically) recovered by cryptanalysis, thus ensuring that the only way security will be compromised is through my own error in coding?

EDIT:

Upon reading all of your answers and further reasearching, I have decided to go ahead and implement the bcrypt method of protecting my passwords. That being said, for curiosity's sake, if I were to take my above code and put a loop on it for say, 100,000 iterations, would that accomplish something similar to the strength/security of bcrypt?


Salts can only help you so far. If the hashing algorithm you use is so fast that there is little to no cost for generating rainbow tables, your security is still compromised.

A few pointers:

  • Do NOT use a single salt for all passwords. Use a randomly generated salt per password.
  • Do NOT rehash an unmodified hash (collision issue, see my previous answer, you need infinite input for hashing).
  • Do NOT attempt to create your own hashing algorithm or mix-matching algorithms into a complex operation.
  • If stuck with broken/unsecure/fast hash primitives, use key strengthening. This increases the time required for the attacker to compute a rainbow table. Example:

function strong_hash($input, $salt = null, $algo = 'sha512', $rounds = 20000) {
  if($salt === null) {
    $salt = crypto_random_bytes(16);
  } else {
    $salt = pack('H*', substr($salt, 0, 32));
  }

  $hash = hash($algo, $salt . $input);

  for($i = 0; $i < $rounds; $i++) {
    // $input is appended to $hash in order to create
    // infinite input.
    $hash = hash($algo, $hash . $input);
  }

  // Return salt and hash. To verify, simply
  // passed stored hash as second parameter.
  return bin2hex($salt) . $hash;
}

function crypto_random_bytes($count) {
  static $randomState = null;

  $bytes = '';

  if(function_exists('openssl_random_pseudo_bytes') &&
      (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN')) { // OpenSSL slow on Win
    $bytes = openssl_random_pseudo_bytes($count);
  }

  if($bytes === '' && is_readable('/dev/urandom') &&
     ($hRand = @fopen('/dev/urandom', 'rb')) !== FALSE) {
    $bytes = fread($hRand, $count);
    fclose($hRand);
  }

  if(strlen($bytes) < $count) {
    $bytes = '';

    if($randomState === null) {
      $randomState = microtime();
      if(function_exists('getmypid')) {
        $randomState .= getmypid();
      }
    }

    for($i = 0; $i < $count; $i += 16) {
      $randomState = md5(microtime() . $randomState);

      if (PHP_VERSION >= '5') {
        $bytes .= md5($randomState, true);
      } else {
        $bytes .= pack('H*', md5($randomState));
      }
    }

    $bytes = substr($bytes, 0, $count);
  }

  return $bytes;
}

Instead of deploying your own (inherently with flaws) hash/salt algorithm, why not use one that was developed by security professionals?

Use bcrypt. It's been developed exactly for this in mind. It slowness and multiple rounds ensures that an attacker must deploy massive funds and hardware to be able to crack your passwords. Add to that per-password salts (bcrypt REQUIRES salts) and you can be sure that an attack is virtually unfeasible without either ludicrous amount of funds or hardware.

The Portable PHP Hashing Framework in non-portable mode allows you to generate hashes using bcrypt easily.

You can also use crypt() function to generate bcrypt hashes of input strings. If you go down that route, make sure you generate one salt per hash.

This class can automatically generate salts and verify existing hashes against an input.

class Bcrypt {
  private $rounds;
  public function __construct($rounds = 12) {
    if(CRYPT_BLOWFISH != 1) {
      throw new Exception("bcrypt not supported in this installation. See http://php.net/crypt");
    }

    $this->rounds = $rounds;
  }

  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() {
    $salt = sprintf('$2a$%02d$', $this->rounds);

    $bytes = $this->getRandomBytes(16);

    $salt .= $this->encodeBytes($bytes);

    return $salt;
  }

  private $randomState;
  private function getRandomBytes($count) {
    $bytes = '';

    if(function_exists('openssl_random_pseudo_bytes') &&
        (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN')) { // OpenSSL slow on Win
      $bytes = openssl_random_pseudo_bytes($count);
    }

    if($bytes === '' && is_readable('/dev/urandom') &&
       ($hRand = @fopen('/dev/urandom', 'rb')) !== FALSE) {
      $bytes = fread($hRand, $count);
      fclose($hRand);
    }

    if(strlen($bytes) < $count) {
      $bytes = '';

      if($this->randomState === null) {
        $this->randomState = microtime();
        if(function_exists('getmypid')) {
          $this->randomState .= getmypid();
        }
      }

      for($i = 0; $i < $count; $i += 16) {
        $this->randomState = md5(microtime() . $this->randomState);

        if (PHP_VERSION >= '5') {
          $bytes .= md5($this->randomState, true);
        } else {
          $bytes .= pack('H*', md5($this->randomState));
        }
      }

      $bytes = substr($bytes, 0, $count);
    }

    return $bytes;
  }

  private function encodeBytes($input) {
    return strtr(rtrim(base64_encode($input), '='), '+', '.');
  }
}

You may use this code as such:

$bcrypt = new Bcrypt(15);

$hash = $bcrypt->hash('password');
$isGood = $bcrypt->verify('password', $hash);


About Salt Values

If I understand correctly, the longer the salt, the larger the table the hacker has to generate in order to break the hash. Please correct me if I am wrong.

Yes, that's correct. Although if someone tries to break a hash of only one user, salt values are useless. Salts are useful for preventing (slowing down) attackers doing dictionary attack on all of your users hash values.

Let me explain that with an example. Suppose you have 3 users in your system and you don't use a salt value, so your database would like this:

user1: hash1
user2: hash2
user3: hash3

Now let's assume that an attacker achieves to get a copy of your database. He could now do a dictionary attack by doing:

h = hash(possible_password)
h == hash1?
h == hash2?
h == hash3?

And so, he could check if one of the 3 users has the password possible_password by only calling the hash function one time.

No suppose you save the hash values which were combined with salt values in your database like this:

user1: hash1_salted, salt1
user2: hash2_salted, salt2
user3: hash3_salted, salt3

And again an attacker copies your database. But now in order to see if possible_password is used by one of the 3 users he must do the following checks:

hash(possible_password + salt1) == hash1_salted?
hash(possible_password + salt2) == hash2_salted?
hash(possible_password + salt3) == hash3_salted?

As you see, in this case the attacker is slowed down by a factor of 3 (the number of users in your system), as he must hash 3 diffferent strings. That's the general idea behind salt values, you could read more on wikipedia.

But in your case, the salt is too big. What you want to prevent is 2 different user hashes to have the same salt value. So, for example a salt of 2 bits length won't probably be a good idea (for more than 4 users it will be sure that 2 have the same salt value). Anyway, a salt value of more than 48 bits will be enough.

Also, there is not really a point in hashing the salt here $salt2 = hash('sha256', $salt);, this could slow things somehow, but in general adding more complexity in your system is considered bad when dealing with security.

General

Finally, it's never good to have specific values in your code when dealing with security, like $secret_server_hash, such constant values should always be avoided.

It's better that you use SHA-2, instead of MD5, because in the recent years some security vulnerabilities have been found in MD5 (although they are not yet really practical).

So I would do something like this:

function createSalt()
{
  $string = hash('sha256', uniqid(rand(), true));
  return susbstr($string, 0, 8); // 8 characters is more than enough
}

$salt = createSalt();
$hash = hash('sha256', $hash . $password );

And then save the $hash at your database.

Anyway, as some users already pointed out. Instead of creating your own security functions (which is a good way for learning about security) you should better use well-known libraries which are tested by a greater amount of people and hence probably more secure. In your case you should have a look at crypt which does what you need.


There's really no need to try to implement your own series of hashes. Here's a simple class that implements bcrypt:

class Password
{
    # return a hashed version of the plain text password.
    public static function hash($plain_text, $cost_factor = 10)
    {
        if ($cost_factor < 4 || $cost_factor > 31)
            throw new Exception('Invalid cost factor');

        $cost_factor = sprintf('%02d', $cost_factor);           

        $salt = '';
        for ($i = 0; $i < 8; ++$i)
          $salt .= pack('S1', mt_rand(0, 0xffff));

        $salt = strtr(rtrim(base64_encode($salt), '='), '+', '.');

        return crypt($plain_text, '$2a$'.$cost_factor.'$'.$salt);
    }

    # validate that a hashed password is the same as the plain text version
    public static function validate($plain, $hash)
    {
        return crypt($plain, $hash) == $hash;
    }
}

Using:

$hash = Password::hash('foo');
if (Password::validate('foo', $hash)) echo "valid";

The upside of bcrypt is that you can make it computationally expensive to hash a password (via $cost_factor). This makes it impractical to try to recover an entire databases' passwords by brute force.

0

精彩评论

暂无评论...
验证码 换一张
取 消