我们如何在 PHP 中创建相当安全的密码哈希?

发布于 2024-11-15 07:42:12 字数 1003 浏览 4 评论 0原文

我一直在阅读有关密码散列的内容,但我读过的所有论坛都充满了人们争论其背后的理论的帖子,而我并不真正理解。

我有一个旧的(可能非常弱)密码脚本,内容如下: $hash = sha1($pass1);

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

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

如果我理解正确的话,盐越长,黑客为了破解哈希值必须生成的表就越大。如果我错了,请纠正我。

我正在寻找编写一个更安全的新脚本,并且我认为这样的事情就可以了:

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 );

这更安全吗?这是否有明显的开销?

最重要的是,是否有更好的方法来确保我的数据库中的密码无法(实际上)通过密码分析恢复,从而确保安全性受到损害的唯一方法是通过我自己的编码错误?

编辑:

在阅读了您的所有答案并进一步研究后,我决定继续实施 bcrypt 方法来保护我的密码。话虽这么说,出于好奇,如果我采用上面的代码并对其进行循环(例如 100,000 次迭代),是否会实现与 bcrypt 的强度/安全性类似的效果?

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?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

审判长 2024-11-22 07:42:12

盐只能帮助你到目前为止。如果您使用的哈希算法非常快,以至于生成彩虹表的成本几乎为零,那么您的安全性仍然会受到损害。

几点提示:

  • 不要对所有密码使用单一盐。每个密码使用随机生成的盐。
  • 不要不要重新散列未修改的哈希(冲突问题,请参阅我之前的回答,您需要无限的输入进行哈希处理)。
  • 不要尝试创建自己的哈希算法或将算法混合到复杂的操作中。
  • 如果遇到损坏/不安全/快速的哈希原语,请使用密钥强化。这增加了攻击者计算彩虹表所需的时间。示例:

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;
}

为什么不使用由安全专业人员开发的算法,而不是部署您自己的(本质上有缺陷的)哈希/盐算法?

使用bcrypt。它正是为此而开发的。它的缓慢性和多轮性确保攻击者必须部署大量资金和硬件才能破解您的密码。添加每个密码的盐(bcrypt 需要盐),您可以确定,如果没有大量的资金或硬件,攻击实际上是不可行的。

非便携式模式下的便携式 PHP 哈希框架允许您轻松使用 bcrypt 生成哈希值。

您还可以使用 crypt() 函数生成输入字符串的 bcrypt 哈希值。如果您沿着这条路线走下去,请确保每个哈希生成一个盐。

此类可以自动生成盐并根据输入验证现有哈希值。

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

您可以这样使用此代码:

$bcrypt = new Bcrypt(15);

$hash = $bcrypt->hash('password');
$isGood = $bcrypt->verify('password', $hash);
, $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), '='), '+', '.'); } }

您可以这样使用此代码:

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

You may use this code as such:

$bcrypt = new Bcrypt(15);

$hash = $bcrypt->hash('password');
$isGood = $bcrypt->verify('password', $hash);
, $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:

挖鼻大婶 2024-11-22 07:42:12

关于盐值

如果我理解正确的话,时间越长
盐越多,桌子越大
黑客必须生成才能
打破哈希。请纠正我,如果我
我错了。

是的,这是正确的。尽管如果有人试图破坏只有一个用户的哈希值,盐值是没有用的。盐对于防止(减慢)攻击者对所有用户哈希值进行字典攻击非常有用。

让我用一个例子来解释一下。假设您的系统中有 3 个用户,并且您不使用 salt 值,那么您的数据库将如下所示:

user1: hash1
user2: hash2
user3: hash3

现在让我们假设攻击者获得了您的数据库的副本。他现在可以通过以下方式进行字典攻击:

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

因此,他只需调用一次哈希函数即可检查 3 个用户之一是否拥有密码 possible_password

不,假设您将与盐值组合的哈希值保存在数据库中,如下所示:

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

攻击者再次复制您的数据库。但现在为了查看 possible_password 是否被 3 个用户之一使用,他必须执行以下检查:

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

如您所见,在这种情况下,攻击者的速度减慢了 3 倍(数字你系统中的用户数),因为他必须散列 3 个不同的字符串。这是盐值背后的一般思想,您可以在 wikipedia 上阅读更多内容。

但就你而言,盐太大了。您想要防止的是 2 个不同的用户哈希具有相同的盐值。因此,例如 2 位长度的盐可能不是一个好主意(对于超过 4 个用户,将确保 2 个用户具有相同的盐值)。无论如何,超过 48 位 的盐值就足够了。

另外,这里对盐进行哈希处理并没有什么意义$salt2 = hash('sha256', $salt);,这可能会以某种方式减慢速度,但总的来说会增加更多的复杂性在处理安全性时,您的系统被认为是糟糕的。

常规

最后,在处理安全问题时,在代码中使用特定值永远不会好,例如 $secret_server_hash ,应始终避免此类常量值。

最好使用 SHA-2,而不是 MD5,因为近年来一些安全性 MD5 中已发现漏洞(尽管尚未真正实用)。

所以我会做这样的事情:

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 );

然后将 $hash 保存在数据库中。

无论如何,正如一些用户已经指出的那样。您应该更好地使用众所周知的库,而不是创建自己的安全函数(这是学习安全性的好方法),这些库经过了更多人的测试,因此可能更安全。在你的情况下,你应该看看 crypt 它可以满足你的需要。

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.

酒绊 2024-11-22 07:42:12

确实没有必要尝试实现您自己的一系列哈希值。下面是一个实现 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

使用:

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

bcrypt 的优点是可以使计算密码的哈希值变得昂贵(通过 $cost_factor)。这使得尝试通过暴力恢复整个数据库的密码变得不切实际。

.$cost_factor.'

使用:


bcrypt 的优点是可以使计算密码的哈希值变得昂贵(通过 $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; } }

使用:

bcrypt 的优点是可以使计算密码的哈希值变得昂贵(通过 $cost_factor)。这使得尝试通过暴力恢复整个数据库的密码变得不切实际。

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

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.

.$cost_factor.'

Using:


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.

.$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:

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.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文