2010-04-07 39 views
1

phpass是一个广泛使用的哈希'框架'。在评估phpass'HashPassword时,我遇到了这个奇怪的方法片段。*如何能成为一个安全的哈希密码?

function HashPassword($password) 
    { 
     // <snip> trying to generate a hash… 

     # Returning '*' on error is safe here, but would _not_ be safe 
     # in a crypt(3)-like function used _both_ for generating new 
     # hashes and for validating passwords against existing hashes. 
     return '*'; 
    } 

答:我们同意这个类假定我们测试我们的哈希值相等的*作为验证的一种手段。这就是为什么我会包装这个类,因为它的界面并不好。如果失败,我预计会失败。


这是完整的phpsalt类:

# Portable PHP password hashing framework. 
# 
# Version 0.2/genuine. 
# 
# Written by Solar Designer <solar at openwall.com> in 2004-2006 and placed in 
# the public domain. 
# 
# 
# 
class PasswordHash { 
    var $itoa64; 
    var $iteration_count_log2; 
    var $portable_hashes; 
    var $random_state; 

    function PasswordHash($iteration_count_log2, $portable_hashes) 
    { 
     $this->itoa64 = './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; 

     if ($iteration_count_log2 < 4 || $iteration_count_log2 > 31) 
      $iteration_count_log2 = 8; 
     $this->iteration_count_log2 = $iteration_count_log2; 

     $this->portable_hashes = $portable_hashes; 

     $this->random_state = microtime() . getmypid(); 
    } 

    function get_random_bytes($count) 
    { 
     $output = ''; 
     if (is_readable('/dev/urandom') && 
      ($fh = @fopen('/dev/urandom', 'rb'))) { 
      $output = fread($fh, $count); 
      fclose($fh); 
     } 

     if (strlen($output) < $count) { 
      $output = ''; 
      for ($i = 0; $i < $count; $i += 16) { 
       $this->random_state = 
        md5(microtime() . $this->random_state); 
       $output .= 
        pack('H*', md5($this->random_state)); 
      } 
      $output = substr($output, 0, $count); 
     } 

     return $output; 
    } 

    function encode64($input, $count) 
    { 
     $output = ''; 
     $i = 0; 
     do { 
      $value = ord($input[$i++]); 
      $output .= $this->itoa64[$value & 0x3f]; 
      if ($i < $count) 
       $value |= ord($input[$i]) << 8; 
      $output .= $this->itoa64[($value >> 6) & 0x3f]; 
      if ($i++ >= $count) 
       break; 
      if ($i < $count) 
       $value |= ord($input[$i]) << 16; 
      $output .= $this->itoa64[($value >> 12) & 0x3f]; 
      if ($i++ >= $count) 
       break; 
      $output .= $this->itoa64[($value >> 18) & 0x3f]; 
     } while ($i < $count); 

     return $output; 
    } 

    function gensalt_private($input) 
    { 
     $output = '$P$'; 
     $output .= $this->itoa64[min($this->iteration_count_log2 + 
      ((PHP_VERSION >= '5') ? 5 : 3), 30)]; 
     $output .= $this->encode64($input, 6); 

     return $output; 
    } 

    function crypt_private($password, $setting) 
    { 
     $output = '*0'; 
     if (substr($setting, 0, 2) == $output) 
      $output = '*1'; 

     if (substr($setting, 0, 3) != '$P$') 
      return $output; 

     $count_log2 = strpos($this->itoa64, $setting[3]); 
     if ($count_log2 < 7 || $count_log2 > 30) 
      return $output; 

     $count = 1 << $count_log2; 

     $salt = substr($setting, 4, 8); 
     if (strlen($salt) != 8) 
      return $output; 

     # We're kind of forced to use MD5 here since it's the only 
     # cryptographic primitive available in all versions of PHP 
     # currently in use. To implement our own low-level crypto 
     # in PHP would result in much worse performance and 
     # consequently in lower iteration counts and hashes that are 
     # quicker to crack (by non-PHP code). 
     if (PHP_VERSION >= '5') { 
      $hash = md5($salt . $password, TRUE); 
      do { 
       $hash = md5($hash . $password, TRUE); 
      } while (--$count); 
     } else { 
      $hash = pack('H*', md5($salt . $password)); 
      do { 
       $hash = pack('H*', md5($hash . $password)); 
      } while (--$count); 
     } 

     $output = substr($setting, 0, 12); 
     $output .= $this->encode64($hash, 16); 

     return $output; 
    } 

    function gensalt_extended($input) 
    { 
     $count_log2 = min($this->iteration_count_log2 + 8, 24); 
     # This should be odd to not reveal weak DES keys, and the 
     # maximum valid value is (2**24 - 1) which is odd anyway. 
     $count = (1 << $count_log2) - 1; 

     $output = '_'; 
     $output .= $this->itoa64[$count & 0x3f]; 
     $output .= $this->itoa64[($count >> 6) & 0x3f]; 
     $output .= $this->itoa64[($count >> 12) & 0x3f]; 
     $output .= $this->itoa64[($count >> 18) & 0x3f]; 

     $output .= $this->encode64($input, 3); 

     return $output; 
    } 

    function gensalt_blowfish($input) 
    { 
     # This one needs to use a different order of characters and a 
     # different encoding scheme from the one in encode64() above. 
     # We care because the last character in our encoded string will 
     # only represent 2 bits. While two known implementations of 
     # bcrypt will happily accept and correct a salt string which 
     # has the 4 unused bits set to non-zero, we do not want to take 
     # chances and we also do not want to waste an additional byte 
     # of entropy. 
     $itoa64 = './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; 

     $output = '$2a$'; 
     $output .= chr(ord('0') + $this->iteration_count_log2/10); 
     $output .= chr(ord('0') + $this->iteration_count_log2 % 10); 
     $output .= '$'; 

     $i = 0; 
     do { 
      $c1 = ord($input[$i++]); 
      $output .= $itoa64[$c1 >> 2]; 
      $c1 = ($c1 & 0x03) << 4; 
      if ($i >= 16) { 
       $output .= $itoa64[$c1]; 
       break; 
      } 

      $c2 = ord($input[$i++]); 
      $c1 |= $c2 >> 4; 
      $output .= $itoa64[$c1]; 
      $c1 = ($c2 & 0x0f) << 2; 

      $c2 = ord($input[$i++]); 
      $c1 |= $c2 >> 6; 
      $output .= $itoa64[$c1]; 
      $output .= $itoa64[$c2 & 0x3f]; 
     } while (1); 

     return $output; 
    } 

    function HashPassword($password) 
    { 
     $random = ''; 

     if (CRYPT_BLOWFISH == 1 && !$this->portable_hashes) { 
      $random = $this->get_random_bytes(16); 
      $hash = 
       crypt($password, $this->gensalt_blowfish($random)); 
      if (strlen($hash) == 60) 
       return $hash; 
     } 

     if (CRYPT_EXT_DES == 1 && !$this->portable_hashes) { 
      if (strlen($random) < 3) 
       $random = $this->get_random_bytes(3); 
      $hash = 
       crypt($password, $this->gensalt_extended($random)); 
      if (strlen($hash) == 20) 
       return $hash; 
     } 

     if (strlen($random) < 6) 
      $random = $this->get_random_bytes(6); 
     $hash = 
      $this->crypt_private($password, 
      $this->gensalt_private($random)); 
     if (strlen($hash) == 34) 
      return $hash; 

     # Returning '*' on error is safe here, but would _not_ be safe 
     # in a crypt(3)-like function used _both_ for generating new 
     # hashes and for validating passwords against existing hashes. 
     return '*'; 
    } 

    function CheckPassword($password, $stored_hash) 
    { 
     $hash = $this->crypt_private($password, $stored_hash); 
     if ($hash[0] == '*') 
      $hash = crypt($password, $stored_hash); 

     return $hash == $stored_hash; 
    } 
} 
+0

为什么不呢?不是最安全的散列方法,但速度非常快。 (只是在开玩笑......) – soulmerge 2010-04-07 22:49:30

回答

7

我不知道你问什么。该评论直接表示它返回一个错误返回*,所以它不是“安全散列密码”,它表示在尝试生成散列时发生错误。一个返回值的奇怪选择,但它就是这样。

+10

它可能受'/ etc/passwd'启发,其中密码字段中的*表示无法登录的帐户(由于*不是任何密码的散列)。 – bobince 2010-04-07 22:51:13

+0

这是因为错误代码,我开始怀疑我是否误解了某些东西。一个无辜的api用户现在需要提前阅读代码,以便了解他需要针对'*'进行测试。感谢您的确认。 – 2010-04-07 22:51:26

+0

@bobince +1这听起来像是这个奇怪代码的一个很好的理由。 – 2010-04-07 22:59:51

2

*可能因错误而返回的原因是*不是任何密码的可能哈希值。因此,返回该值将会产生一个显然不是真正的哈希值的值,并且不可能与另一个哈希值相匹配。

+0

除非你不把它当作错误。如果用户输入了一个导致'*'的密码,并将其保存为数据库中的哈希值。然后你对验证'*'=='*'也做同样的事情,以便测试通过。 如果发生错误,最好知道'*'应该被视为错误。如果不行,为什么要在代码上进行规划?至少应该记录在案。 或者我在想错? (这不会是今天的第一次...) – Savageman 2010-04-07 22:50:25

+0

同意。但是对于这种情况,我会期望'false','null'或''''。 – 2010-04-07 22:53:56

+0

@Savageman,注意代码示例中关于'crypt(3)'的注释。这是安全的原因是因为该函数并不是用于比较的哈希,而只是用于存储的哈希。 – 2010-04-07 23:35:25