密码哈希 - 如何升级?

28

关于最佳算法,有很多讨论 - 但如果您已经在生产中,如何升级而无需重置用户?


编辑/免责声明:尽管我最初想要一个“快速修复”解决方案并选择了orip的回答,但必须承认,如果您的应用程序中的安全性足够重要以至于要处理此问题,则快速修复是错误的心态,他提出的解决方案可能是不充分的。


2
从哪里升级到哪里?如果你从“未使用哈希”升级到“使用哈希”,那么就没问题了。 - Greg Hewgill
1
@Greg 哈希到哈希 - 我真的希望 SO 上没有人存储明文 :) - Anson Kao
2
你得到了错误的答案,实现仍然容易受到碰撞攻击。下次用户登录时,只需对明文进行哈希处理即可。 - rook
@Root:把这个奖项颁给“John” Skeet将对Jon Skeet非常不公平。 - Steven Sudit
@Steven Sudit:LOL 是 "@Root" 而不是 "@Rook",这是有意的吗? - Anson Kao
显示剩余2条评论
6个回答

25

一种选择是在存储的哈希值中加入算法版本号 - 因此你可以从算法0(例如MD5)开始存储。

0:ab0123fe

然后当你升级到SHA-1时,你将版本号提高到1:

1:babababa192df1312

不,我知道这些长度可能不正确。

这样你就可以始终知道要检查哪个版本来验证密码。你可以通过清除以该版本号开头的存储散列来使旧的算法失效。

如果您已经在生产中使用哈希而没有版本号,只需选择一种方案,使得您可以轻松地识别未经版本化的哈希 - 例如,使用上述带有冒号的方案,任何不包含冒号的哈希必定是版本控制之前的,因此可以被推断为版本0(或其他版本号)。


6
我的第一个想法是将哈希版本号存储在自己的字段中。不作修辞:为什么要将它包含在字符串中呢? - Matchu
2
@Matchu:由于它们缺一不可,将它们存储在同一字段中可以确保您永远不会遇到只有其中一个而没有另一个的情况。 - Amber
4
@Matchu,基本上是同一件事情,但你是对的,将其存储在单独的字段中将更容易查询表格以查看有多少用户已经升级。 - Anson Kao
1
这会在数据库中保留旧的弱哈希,至少直到用户下次登录。因此,在此基础上应该使用双哈希技术(类似于 orip 的帖子,但加入了盐)来处理旧哈希。 - CodesInChaos
1
@Slartibartfast:除非你碰巧使用了 "crypt"、"md5" 或者 "sha",否则它基本上等同于使用 RFC 2307...那么 RFC 2307 是一个好的计划,在何种程度上等效的计划不是呢?它们两者基本上都将用于执行哈希的算法与哈希本身一起保存。 - Jon Skeet
显示剩余4条评论

22
使用现有哈希作为新的、更好的密码哈希的输入,是一种安全所有现有密码的酷方法。
因此,如果您现有的哈希是直接MD5,而您计划转移到某种PBKDF2(或bcrypt,或scrypt),那么请将您的密码哈希更改为:
PBKDF2( MD5( password ) )

您的数据库中已经有MD5,所以您需要对其应用PBKDF2。

这种方法之所以有效是因为MD5相对于其他哈希函数(例如SHA-*)的弱点并不影响密码使用。例如,它的碰撞漏洞对数字签名来说是毁灭性的,但对密码哈希没有影响。与更长的哈希值相比,MD5的128位输出会略微减少哈希搜索空间,但相对于密码搜索空间本身而言,这是微不足道的。

使密码哈希强大的因素是通过放慢迭代次数(在PBKDF2中实现)和添加随机、足够长的盐来实现的 - 初始MD5并不会对它们造成负面影响。

顺便说一句,在密码中添加一个版本字段也是一个好主意。

编辑:加密StackExchange上有一个有趣的讨论关于这个方法。


1
+1 这正是我在寻找的,所有其他回复都需要很长时间才能完成升级,并且可能需要重置长时间不登录的用户。 - Anson Kao
我的解决方案(以及几乎所有其他人的解决方案)需要所有用户登录。 - mellowsoon
4
@MicE - 防范暴力攻击的安全性取决于其最薄弱的环节,而在这种情况下是密码本身,而不是哈希值。密码的范围远小于哈希值的范围,重新散列(虽然理论上会损失一些范围)与之相比微不足道。事实上,在内部,PBKDF2会将密码重新散列数千次或更多。例如,要在密码中获得128位熵的等效值,您需要使用所有可用键盘字符(包括所有特殊字符)生成一个20个字符完全随机的密码。 - orip
1
这个方案中的盐在哪里? - Steven Sudit
@orip @Steven Sudit 阅读 John Skeet 的帖子或者我的帖子,然后告诉我哪种解决方案更好。 - rook
显示剩余21条评论

11

等待用户登录(这样你就可以得到未加密的密码), 然后使用新算法将其进行哈希处理并保存在数据库中。


2

一种方法是:

  • 为新密码引入新字段
  • 用户登录时检查密码是否与旧哈希值匹配
  • 如果匹配,用新哈希值对明文密码进行哈希处理
  • 删除旧的哈希值

这样,渐渐地,你将只有使用新哈希值的密码。


0
你现在可能无法更改密码哈希方案,除非你正在以纯文本形式存储密码。你可以在每个用户成功登录后,重新使用更好的哈希方案重新哈希会员密码。

你可以尝试这样做:

首先,在你的成员表或存储密码的任何表中添加一个新列。

ALTER TABLE members ADD is_pass_upgraded tinyint(1) default 0;

接下来,在验证用户的代码中,添加一些额外的逻辑(我使用的是PHP):
<?php
$username = $_POST['username'];
$password = $_POST['password'];

$auth_success = authenticateUser($username, $password); 
if (!$auth_success) {
    /**
     * They entered the wrong username/password. Redirect them back
     * to  the login page.
     */
} else {
    /**
     * Check to see if the member's password has been upgraded yet
     */
    $username = mysql_real_escape_string($username);
    $sql = "SELECT id FROM members WHERE username = '$username' AND is_pass_upgraded = 0 LIMIT 1";
    $results = mysql_query($sql);

    /**
     * Getting any results from the query means their password hasn't been
     * upgraded yet. We will upgrade it now.
     */
    if (mysql_num_rows($results) > 0) {
        /**
         * Generate a new password hash using your new algorithm. That's
         * what the generateNewPasswordHash() function does.
         */
        $password = generateNewPasswordHash($password);
        $password = mysql_real_escape_string($password);

        /**
         * Now that we have a new password hash, we'll update the member table
         * with the new password hash, and change the is_pass_upgraded flag.
         */
        $sql = "UPDATE members SET password = '$password', is_pass_upgraded = 1 WHERE username = '$username' LIMIT 1";
        mysql_query($sql);
    }
}

您的authenticateUser()函数需要更改为类似于以下内容:

<?php
function authenticateUser($username, $password)
{
    $username = mysql_real_escape_string($username);

    /**
     * We need password hashes using your old system (md5 for example)
     * and your new system.
     */
    $old_password_hashed = md5($password);
    $new_password_hashed = generateBetterPasswordHash($password);
    $old_password_hashed = mysql_real_escape_string($old_password_hashed);
    $new_password_hashed = mysql_real_escape_string($new_password_hashed);

    $sql = "SELECT *
        FROM members
        WHERE username = '$username'
        AND
        (
            (is_pass_upgraded = 0 AND password = '$old_password_hashed')
            OR
            (is_pass_upgraded = 1 AND password = '$new_password_hashed')
        )
        LIMIT 1";
    $results = mysql_query($sql);
    if (mysql_num_rows($results) > 0) {
        $row = mysql_fetch_assoc($results);
        startUserSession($row);
        return true;
    } else {
        return false;
    }
}

这种方法有优点和缺点。优点是,个人成员登录后,其密码变得更加安全。缺点是,每个人的密码都没有得到保护。

我只会这样做大约两周时间。我会给所有成员发送电子邮件,并告诉他们由于网站升级,他们有2周时间登录他们的帐户。如果他们在2周内未能登录,则需要使用密码恢复系统重置密码。


1
盐在哪里?另外,为什么要有一个单独的标志字段而不是前缀(正如其他人所建议的)? - Steven Sudit

0

当他们下一次进行身份验证时,只需重新散列原始文本。同时使用基于256的盐(完整字节)和大小为256字节的SHA-256。


1
SHA-256的设计目标之一是“快速”。但是,“快速”并不是密码哈希的理想特性。适应成本哈希方案(例如BCrypt哈希)更适合用于密码哈希。 - Jacco
@Jacco 如果攻击者无法承受太重的负载,那么你的 Web 服务器也无法承受。NIST 不会批准任何缓慢的消息摘要函数。请查看 PBKDF2 的维基页面,它仅用于一次只有一个用户登录的应用程序。 - rook
Rook,再次提醒您,您至少应该提到使用盐的作用。 - Steven Sudit
@Steven Sudit,你应该假设我的代码和建议都不违反CWE。 - rook
Rook,你的实现是否存在违规并不是问题:由于遗漏,你描述的系统确实存在违规。我建议你至少提及随机加盐,或者使用HMAC来混合盐。 - Steven Sudit
显示剩余8条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接