14

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, 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?

BumbleShrimp
  • 2,150
  • 23
  • 42
  • That is more than secure enough. As long as your salt is ~ 32-64 characters of random data you are going to be fine, although you will run into problems when validating with that `createSalt` method because it will always return a different value. So if you try to validate it when they sign in you wont likely get the same hash out of sha512. – Bob_Gneu Jun 14 '11 at 07:03
  • 6
    http://codahale.com/how-to-safely-store-a-password/ – Cole Jun 14 '11 at 07:07
  • @Cole I think that link should have been an answer ;) – Yoshi Jun 14 '11 at 07:19
  • @Bob_Gneu Thank you for answering! I should have been more clear, this is the code to create a new password and store it (along with that specific salt) in the database for that user. After that, for log-in attempts, the password would be re-hashed with the salt from the database. – BumbleShrimp Jun 14 '11 at 07:39

3 Answers3

11

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);
Community
  • 1
  • 1
Andrew Moore
  • 93,497
  • 30
  • 163
  • 175
  • In this case, I need to also store Bcrypt->getSalt() in my database as the password is created, do I not? – BumbleShrimp Jun 14 '11 at 07:35
  • @user797115: No, the salt is stored within the `bcrypt` hash. All you need to do is store the resulting hash and verify using the `->verify()` method (see example). Also, I just corrected a missing `return` statement in the class, so you might want to copy/paste again. – Andrew Moore Jun 14 '11 at 07:38
  • I'm confused, how does the verify function work if there is no salt stored when the password is created? It looks like the salt is randomly generated? – BumbleShrimp Jun 14 '11 at 07:52
  • @user797115, The salt is stored as part of the password hash. i.e., The return value of `hash()` will contain the salt along with the entire hash. – Matthew Jun 14 '11 at 08:11
  • Thank you, I didn't realize that. My last question for you (if you wouldn't mind answering) is this: Does the fact that crypt() by default only uses a 2 character salt have any adverse affect on the security? As it turns out my server does not support bcrypt, and so I might use crypt(), but would doing crypt with sha256 instead of the standard be more secure, or does it really matter? – BumbleShrimp Jun 14 '11 at 08:25
  • @user787115: Then you are better off using either the `strong_hash` function provided in my answer or using [Portable PHP Hashing Framework](http://www.openwall.com/phpass/) in portable mode. Also, `crypt()` doesn't have a default salt length. It depends of the algorithm used. The `$2a$` or other are simply the algorithm signature so that `crypt()` can identify the one used when verifying. It isn't a salt. The salt is appended to the algo signature. – Andrew Moore Jun 14 '11 at 08:59
4

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.

insumity
  • 5,311
  • 8
  • 36
  • 64
  • Thanks for answering! The only reason I had the "secret" salt in there was as a back-up measure to further hamper an attacker who only has access to the database but not the source code. I've just recently begun reading up on this, but it seemed to me at the time of writing this article that keeping the salt secret would be even better than showing it, so I thought I would combine the two ideas to get the best of both worlds. – BumbleShrimp Jun 14 '11 at 07:41
  • @user797115 I see. Well, salt values can't be "secret" because then you don't have a way to check the user's hash is correct. You had: `$hash = $salt2 . $hash . $secret_server_hash; $hash = hash('sha512', $hash );`, and you saved `$hash` into your database. Then a user tries for example to login, how can you create the hash value again to compare it with the saved `$hash` in order to decide whether the user is valid or not? You need to have somewhere the salt saved (in your database) in order to create again the hash value and do the comparison. – insumity Jun 14 '11 at 07:44
  • I haven't been clear enough in my question, I apologize. I of course would be storing the randomly generated salt in the database upon creation of the password hash for the first time. Also, the "secret" salt would remain unchanged for that particular website indefinitely. – BumbleShrimp Jun 14 '11 at 07:48
  • Aha. Well I suppose you could hash one more time the salt as you did. But there is not really a reason to do so. As I said in my answer, the more complex your code (logic) is, the more easy for an attacker to find a hole. Anyway, hopefully my answer helped you :) – insumity Jun 14 '11 at 07:52
  • Even if the hacker didn't have the secret salt, he could likely find it very easily by cracking his own password that he knows. (That is, if he has the computing power to crack thousands of salted passwords, then he probably has the computing power to crack one salt.) – Matthew Jun 14 '11 at 08:13
2

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.

Matthew
  • 47,584
  • 11
  • 86
  • 98
  • After reading all these answers, and doing some more google searches based on the information you have given me, I definitely plan to implement bcrypt! Thank you for answering. – BumbleShrimp Jun 14 '11 at 07:42
  • @Andrew, 128-bits translates to 22 characters of base64 encoding (minus the padding), so nothing is lost. base64 only works on 8 bits at a time, so while 22 characters could theoretically hold 132-bits of information, that would be an invalid (extraneous) string. That is, `base64_decode('//////////////////////==') == base64_decode('/////////////////////w==')`. If you generate your own 22 char string by hand that takes 132-bits, you are actually silently losing 4-bits, which means some of your random salts are more likely to occur than others. – Matthew Jun 14 '11 at 08:03
  • To easily see my point, check that `var_dump(crypt('a', '$2a$04$zzzzzzzzzzzzzzzzzzzzzz') == crypt('a', '$2a$04$zzzzzzzzzzzzzzzzzzzzzu'));` displays true. So the only valid way to generate the hash is to base64 encode 128-bits of random data. (Although technically, the shortcut of replacing `+` with `.` should be done with a proper `strtr` that slides all the characters over from PHP's to crypt's alphabet, but that really has nothing to do with any fundamental problem of `base64_encode`.) – Matthew Jun 14 '11 at 08:25