【问题标题】:Is Blowfish necessary for security token in PHP remember me system?PHP 中的安全令牌是否需要 Blowfish 记住我系统?
【发布时间】:2013-06-20 22:57:32
【问题描述】:

我搜索过 SO,我知道有很多“记住我”的问题,但我找不到一个专门关于使用 Blowfish 生成安全令牌的问题。

我正在实现一个登录系统以使用基于 cookie 的“记住我”功能,而我读过的所有教程要么看起来不安全(例如,只是基于时间的未加盐 md5 哈希)要么看起来过于复杂.

我不是为银行或类似的东西建立网站,而只是想要一个合理级别的安全性。

我建议的系统是简单地为用户名创建一个 128 字符的随机字符串,并为登录令牌创建第二个 128 字符的字符串。原始字符串将存储在 cookie 中,未加盐的 sha1'd 版本将放在数据库中用户帐户的行中。

我想我什至可以在每次页面加载时重新生成字符串。

对我来说,这提供了不错的安全性(我认为!),因为:

  1. 黑客无法锁定特定帐户,但知道他们的用户名(他们需要知道 128 个字符的字符串)
  2. 要伪造登录 cookie 并获得对帐户的访问权限,有人必须猜测 2 x 128 字符字符串。
  3. 如果数据库被黑,为 128 个字符字符串创建彩虹表太难了。
  4. 只有将实际用户名存储在 cookie 中,我才能安全地记住用户名。

我的问题是:

  1. 这听起来相当安全吗?我应该使用超过 128 个字符的字符串吗?
  2. 是否值得使用 Blowfish 而不是 sha1? (也就是说,我说彩虹桌太难制作了吗?)
  3. 如果没有,运行 sha1 说 1000 次有什么好处吗?

非常感谢。

【问题讨论】:

    标签: php hash


    【解决方案1】:

    嗯,河豚可能意味着两件事。 Cipher(双向加密算法)或Hash(单向哈希算法,具体称为bcrypt)。但稍后会详细介绍。

    那么让我们看看你假设的优势。

    1. 黑客无法通过知道用户名来锁定特定帐户。

      即使他们知道用户名,也没关系。 128 位随机字符串足够大,您真的不需要担心攻击者会猜到它。让我们做一些数学运算。

      2^128 == 3 * 10^38 possible combinations
      
      Assuming there are 50 million servers on the internet,
      And each server can do 100,000,000,000 guesses per second (to your server)
      
      3e38 / 50,000,000 / 100,000,000,000 == 6 * 10^19 seconds
      
      Which when converted to years is: 1,902,587,519,025
      
      To have a 50% chance of guessing it, the attacker would need to hit 1/2:
      
      Years to 50% chance of guessing: 951,293,759,512
      

      因此,除非您担心攻击者会在未来数万亿年内试图侵入您的系统,否则 128 位足够足够强大...

    2. 要伪造登录 cookie ...必须猜测 2 x 128 字符字符串

      我们已经证明猜测 1 不会发生。所以加一秒没必要(可能还不错,但没必要)

    3. 如果数据库被黑客入侵,为 128 个字符字符串创建彩虹表太难了

      是的。然而,这不是你应该关心的。你应该关心的是暴力破解。但稍后会详细介绍...

    所以,回答你的实际问题:

    1. 这听起来是否相当安全。

      合理,当然。过于复杂:绝对。有更简单的方法来处理这个......

    2. 是否值得使用 Blowfish 而不是 sha1。

      没有。 Blowfish 用于推导(意味着您想要工作证明的地方)。在这种情况下,您要生成 MAC(机器验证码)。所以我不会使用 Blowfish 或 SHA1。我会使用 SHA256 或 SHA512...

    3. 运行 sha1 1000 次有优势吗?

      没有

    更好的方法

    我推荐的更好的方法是将 cookie 存储为三个部分。

    function onLogin($user) {
        $token = GenerateRandomToken(); // generate a token, should be 128 - 256 bit
        storeTokenForUser($user, $token);
        $cookie = $user . ':' . $token;
        $mac = hash_hmac('sha256', $cookie, SECRET_KEY);
        $cookie .= ':' . $mac;
        setcookie('rememberme', $cookie);
    }
    

    然后,验证:

    function rememberMe() {
        $cookie = isset($COOKIE['rememberme']) ? $COOKIE['rememberme'] : '';
        if ($cookie) {
            list ($user, $token, $mac) = explode(':', $cookie);
            if ($mac !== hash_hmac('sha256', $user . ':' . $token, SECRET_KEY)) {
                return false;
            }
            $usertoken = fetchTokenByUserName($user);
            if (timingSafeCompare($usertoken, $token)) {
                logUserIn($user);
            }
        }
    }
    

    现在,SECRET_KEY 是一个密码机密(由 /dev/random 之类的东西生成和/或从高熵输入派生)是非常重要的。此外,GenerateRandomToken() 需要是强随机源(mt_rand() 还不够强。使用库或带 DEV_URANDOM 的 mcrypt)...

    而timingSafeCompare是为了防止timing attacks。像这样的:

    /**
     * A timing safe equals comparison
     *
     * To prevent leaking length information, it is important
     * that user input is always used as the second parameter.
     *
     * @param string $safe The internal (safe) value to be checked
     * @param string $user The user submitted (unsafe) value
     *
     * @return boolean True if the two strings are identical.
     */
    function timingSafeCompare($safe, $user) {
        // Prevent issues if string length is 0
        $safe .= chr(0);
        $user .= chr(0);
    
        $safeLen = strlen($safe);
        $userLen = strlen($user);
    
        // Set the result to the difference between the lengths
        $result = $safeLen - $userLen;
    
        // Note that we ALWAYS iterate over the user-supplied length
        // This is to prevent leaking length information
        for ($i = 0; $i < $userLen; $i++) {
            // Using % here is a trick to prevent notices
            // It's safe, since if the lengths are different
            // $result is already non-0
            $result |= (ord($safe[$i % $safeLen]) ^ ord($user[$i]));
        }
    
        // They are only identical strings if $result is exactly 0...
        return $result === 0;
    }
    

    【讨论】:

    • 你怎么能这么快! +1
    • 我知道,很好的答案,非常感谢您花在这上面的所有时间。
    • @ircmaxell 您能否解释一下 HMAC 的安全优势?为什么不简单地将未散列的令牌和用户名存储在 cookie 中,并将散列值存储在数据库中?如果您这样做,那么您是否仍然不需要 HMAC 就可以获得 256 位安全性?谢谢。
    【解决方案2】:

    据我判断,关键是如何生成随机标记,以及在此字符串中允许使用什么字母。假设你使用操作系统的随机源来生成这个 128 个字符的字符串,并且允许区分大小写的字符和数字,你会得到一个非常强大且不可预测的“密码”。

    虽然我的感觉告诉我使用 BCrypt(没有理由反对它),但纯逻辑表明:

    1. 128 个字符比通常的加盐密码长得多,因此不需要加盐。
    2. 暴力破解 62^128 = 2E229 组合是不可能的。由于令牌的随机性,字典攻击也不成问题。这意味着不需要迭代哈希函数。

    不过,我不会使用 SHA-1,这会将安全令牌的熵降低到 160 位。创建一个超强的令牌是没有意义的,只存储它的一部分,而不是使用 SHA-256 或 SHA-512。

    还有其他需要考虑的,配置cookie,所以只能通过HTTPS发送。实际上,您将面临与维护会话相同的问题。

    【讨论】:

      【解决方案3】:

      如果您使用 SSL 创建安全连接来交换令牌,那么我会说这是相当安全的。我只会使用类似 guid 的东西作为令牌,而不是河豚或 SHA1。当你真正想要的是一大串随机的比特时,这些真的什么也得不到。

      如果您一开始没有创建安全连接,那么中间人攻击很可能而且很容易实现。由于 SSL 每次都会创建一个随机密钥,因此确实没有理由在每次页面加载时更改令牌。

      【讨论】:

        猜你喜欢
        • 2012-12-06
        • 2015-11-27
        • 2011-04-08
        • 2012-10-26
        • 2011-07-15
        • 2012-12-17
        • 1970-01-01
        • 1970-01-01
        • 2021-03-21
        相关资源
        最近更新 更多