“双重哈希”比仅哈希一次密码更不安全吗?

324

将密码在存储前进行两次哈希,与仅进行一次哈希相比,哪种更安全?

我所说的是这样做:

$hashed_password = hash(hash($plaintext_password));

而不仅仅是这样:

$hashed_password = hash($plaintext_password);

如果安全性较低,你能提供一个好的解释(或链接)吗?
另外,使用的哈希函数是否有所不同?如果混合使用md5和sha1(例如),而不是重复相同的哈希函数,是否会有所不同?
注意1:当我说“双重哈希”时,我指的是尝试两次哈希密码以使其更加模糊。我不是在谈论解决冲突的技术
注意2:我知道我需要添加一个随机盐来确保安全。问题是使用相同算法两次哈希是否有帮助还是有害。

3
Hash(password)Hash(Hash(password))同样不安全。两者都缺乏语义安全性的概念,即输出不足以与随机数据区分开来。例如,MD5("password")的结果是5f4dcc3b5aa765d61d8327deb882cf99。我知道这是password的MD5哈希值,并且可以从随机值中区分出来。相反,您应该使用HMAC。它具有可证明的安全性并且是一个PRF。 - jww
16个回答

293

一次哈希密码不安全

不,多次哈希并不会降低安全性,它们是确保密码安全使用的必要部分。

迭代哈希可以增加攻击者在其候选密码列表中尝试每个密码所需的时间。您可以轻松地将攻击密码所需的时间从几小时增加到数年。

简单迭代不够安全

仅仅将哈希输出链接到输入并不足以保证安全。迭代应该在保留密码熵的算法上进行。幸运的是,有几种已经受到充分审查以对其设计产生信心的公开算法。

像PBKDF2这样的良好密钥派生算法会将密码注入到每一轮哈希中,缓解了哈希输出碰撞的担忧。PBKDF2可以直接用于密码身份验证。Bcrypt在密钥派生后跟随一个加密步骤;这样,如果发现了反转密钥派生的快速方法,攻击者仍然需要完成已知明文攻击。

如何破解密码

存储的密码需要防范离线攻击。如果密码没有被盐化,它们可以使用预计算的字典攻击(例如使用彩虹表)破解。否则,攻击者必须花费时间为每个密码计算哈希值并查看是否与存储的哈希值匹配。

并非所有密码都是同等可能的。攻击者可能会穷举所有短密码,但他们知道随着每个额外字符的增加,暴力破解成功的机会急剧下降。相反,他们使用最有可能的密码的有序列表。他们从“password123”开始,然后逐渐转向不太常用的密码。

假设攻击者的列表很长,有100亿个候选项;同时假设桌面系统每秒可以计算1百万个哈希值。如果只使用一次迭代,攻击者可以在不到三小时内测试完整个列表。但是,如果使用2000次迭代,这个时间将延长到近8个月。要打败更为复杂的攻击者——例如那些能够下载可以利用GPU性能的程序的攻击者——你需要更多的迭代。
使用的迭代次数是安全性和用户体验之间的权衡。攻击者可以使用的专门硬件很便宜,但它仍然可以每秒执行数亿次迭代。攻击者系统的性能决定了在给定数量的迭代下破解密码所需的时间。但是,你的应用程序不太可能使用这种专门硬件。你可以根据你的系统来确定在不引起用户不满的情况下可以执行多少次迭代。
你可以让用户在认证期间多等待大约3/4秒钟左右。对你的目标平台进行分析,并尽可能多地使用迭代。我测试过的平台(一个移动设备上的用户或一个服务器平台上的多个用户)可以轻松支持PBKDF2 60000到120000次迭代,或bcrypt 12或13的成本因素。

更多背景信息

阅读PKCS#5以获取有关在哈希中使用盐和迭代的作用的权威信息。尽管PBKDF2旨在从密码生成加密密钥,但它也可以作为密码验证的单向哈希函数。每个bcrypt的迭代比SHA-2哈希更昂贵,因此您可以使用较少的迭代,但其思想是相同的。Bcrypt还比大多数基于PBKDF2的解决方案更进一步,通过使用派生密钥对已知明文进行加密。生成的密文与一些元数据一起存储为“哈希值”。但是,您也可以使用PBKDF2执行相同的操作。

以下是我在此主题上撰写的其他答案:


74
当你试图防止对已被攻击的身份验证存储进行字典攻击时,有意制作缓慢的算法是一种可接受的做法。这种技术被称为“密钥加强”或“密钥延伸”。详见http://en.wikipedia.org/wiki/Key_stretching。 - user41871
19
@RoBorg:重要的不是你的实现有多慢,而是攻击者的实现会有多慢:如果哈希本身慢上千倍,攻击者破解密码需要的时间也会慢上千倍。 - orip
6
如果哈希算法的2^128个输出空间完美无误,那么理论上,你会希望在0到2^128-1的128位空间内发生碰撞。这就意味着你只是拥有一个由2^128个符号组成的替换密码字母表。请注意,不应该改变原文意思。 - jmucchiello
15
@devin-- 这不是“我的解决方案”,而是一种被广泛接受的做法,已经内置于基于密码的加密标准(如PKCS#5)中,并得到像Robert Morris这样的专家的推荐。它非常具有可伸缩性,在合法应用程序中认证用户所花费的时间很少。只有在你的应用程序正在破解密码时,才会变得难以扩展-因此需要建议。当然,哈希的搜索空间比可能的密码小,但即使128位空间也太大了,无法进行暴力搜索。需要防御的威胁是离线字典攻击。 - erickson
7
我所提到的不是对个别用户的不便,而是当你有大量用户时服务器会承受的压力,因为你依靠CPU负载来减缓请求的数量。这意味着,如果你增加更多的CPU功率,你就会减少那些暴力攻击者的限制。-- 但是,你完全正确关于可伸缩性和广泛接受的做法。我之前评论中几乎所有的话都是错的。抱歉 :) - DevinB
显示剩余20条评论

246
对于那些认为它安全的人,他们通常是正确的。如果正确执行“双重”哈希(或其逻辑扩展——迭代哈希函数),则绝对是安全的,针对特定问题。
对于那些认为它不安全的人,在这种情况下,他们是正确的。在问题中发布的代码是不安全的。让我们来谈谈原因:
$hashed_password1 = md5( md5( plaintext_password ) );
$hashed_password2 = md5( plaintext_password );

哈希函数有两个基本属性需要我们关注:

  1. 预像阻力 - 给定一个哈希值$h,很难找到一条消息$m使得$h === hash($m)

  2. 第二预像阻力 - 给定一条消息$m1,很难找到另一条不同的消息$m2使得hash($m1) === hash($m2)

  3. 碰撞阻力 - 很难找到一对消息($m1, $m2)使得hash($m1) === hash($m2)(注意这与第二预像阻力类似,但不同之处在于攻击者可以控制两个消息)...

对于密码的存储,我们真正关心的只是Pre-Image Resistance。其他两个属性将是无意义的,因为$m1是我们试图保护的用户密码。因此,如果攻击者已经拥有它,哈希没有任何保护作用...

免责声明

以下所有内容都基于我们只关心Pre-Image Resistance这一前提。哈希函数的其他两个基本属性可能不会(并通常不会)以同样的方式保持。因此,本文的结论仅适用于使用哈希函数存储密码的情况。它们不适用于一般情况...

让我们开始吧

为了讨论方便,让我们发明自己的哈希函数:

function ourHash($input) {
    $result = 0;
    for ($i = 0; $i < strlen($input); $i++) {
        $result += ord($input[$i]);
    }
    return (string) ($result % 256);
}

现在这个哈希函数的作用应该很明显了。它将输入的每个字符的ASCII值相加,然后将结果取模256。
所以让我们来测试一下:
var_dump(
    ourHash('abc'), // string(2) "38"
    ourHash('def'), // string(2) "47"
    ourHash('hij'), // string(2) "59"
    ourHash('klm')  // string(2) "68"
);

现在,让我们看看如果我们在一个函数周围运行它几次会发生什么:
$tests = array(
    "abc",
    "def",
    "hij",
    "klm",
);

foreach ($tests as $test) {
    $hash = $test;
    for ($i = 0; $i < 100; $i++) {
        $hash = ourHash($hash);
    }
    echo "Hashing $test => $hash\n";
}

这将输出:

Hashing abc => 152
Hashing def => 152
Hashing hij => 155
Hashing klm => 155

哎呀,哇。我们生成了碰撞!让我们试着看看为什么:

这是将每个可能的哈希输出字符串进行哈希处理的输出结果:

Hashing 0 => 48
Hashing 1 => 49
Hashing 2 => 50
Hashing 3 => 51
Hashing 4 => 52
Hashing 5 => 53
Hashing 6 => 54
Hashing 7 => 55
Hashing 8 => 56
Hashing 9 => 57
Hashing 10 => 97
Hashing 11 => 98
Hashing 12 => 99
Hashing 13 => 100
Hashing 14 => 101
Hashing 15 => 102
Hashing 16 => 103
Hashing 17 => 104
Hashing 18 => 105
Hashing 19 => 106
Hashing 20 => 98
Hashing 21 => 99
Hashing 22 => 100
Hashing 23 => 101
Hashing 24 => 102
Hashing 25 => 103
Hashing 26 => 104
Hashing 27 => 105
Hashing 28 => 106
Hashing 29 => 107
Hashing 30 => 99
Hashing 31 => 100
Hashing 32 => 101
Hashing 33 => 102
Hashing 34 => 103
Hashing 35 => 104
Hashing 36 => 105
Hashing 37 => 106
Hashing 38 => 107
Hashing 39 => 108
Hashing 40 => 100
Hashing 41 => 101
Hashing 42 => 102
Hashing 43 => 103
Hashing 44 => 104
Hashing 45 => 105
Hashing 46 => 106
Hashing 47 => 107
Hashing 48 => 108
Hashing 49 => 109
Hashing 50 => 101
Hashing 51 => 102
Hashing 52 => 103
Hashing 53 => 104
Hashing 54 => 105
Hashing 55 => 106
Hashing 56 => 107
Hashing 57 => 108
Hashing 58 => 109
Hashing 59 => 110
Hashing 60 => 102
Hashing 61 => 103
Hashing 62 => 104
Hashing 63 => 105
Hashing 64 => 106
Hashing 65 => 107
Hashing 66 => 108
Hashing 67 => 109
Hashing 68 => 110
Hashing 69 => 111
Hashing 70 => 103
Hashing 71 => 104
Hashing 72 => 105
Hashing 73 => 106
Hashing 74 => 107
Hashing 75 => 108
Hashing 76 => 109
Hashing 77 => 110
Hashing 78 => 111
Hashing 79 => 112
Hashing 80 => 104
Hashing 81 => 105
Hashing 82 => 106
Hashing 83 => 107
Hashing 84 => 108
Hashing 85 => 109
Hashing 86 => 110
Hashing 87 => 111
Hashing 88 => 112
Hashing 89 => 113
Hashing 90 => 105
Hashing 91 => 106
Hashing 92 => 107
Hashing 93 => 108
Hashing 94 => 109
Hashing 95 => 110
Hashing 96 => 111
Hashing 97 => 112
Hashing 98 => 113
Hashing 99 => 114
Hashing 100 => 145
Hashing 101 => 146
Hashing 102 => 147
Hashing 103 => 148
Hashing 104 => 149
Hashing 105 => 150
Hashing 106 => 151
Hashing 107 => 152
Hashing 108 => 153
Hashing 109 => 154
Hashing 110 => 146
Hashing 111 => 147
Hashing 112 => 148
Hashing 113 => 149
Hashing 114 => 150
Hashing 115 => 151
Hashing 116 => 152
Hashing 117 => 153
Hashing 118 => 154
Hashing 119 => 155
Hashing 120 => 147
Hashing 121 => 148
Hashing 122 => 149
Hashing 123 => 150
Hashing 124 => 151
Hashing 125 => 152
Hashing 126 => 153
Hashing 127 => 154
Hashing 128 => 155
Hashing 129 => 156
Hashing 130 => 148
Hashing 131 => 149
Hashing 132 => 150
Hashing 133 => 151
Hashing 134 => 152
Hashing 135 => 153
Hashing 136 => 154
Hashing 137 => 155
Hashing 138 => 156
Hashing 139 => 157
Hashing 140 => 149
Hashing 141 => 150
Hashing 142 => 151
Hashing 143 => 152
Hashing 144 => 153
Hashing 145 => 154
Hashing 146 => 155
Hashing 147 => 156
Hashing 148 => 157
Hashing 149 => 158
Hashing 150 => 150
Hashing 151 => 151
Hashing 152 => 152
Hashing 153 => 153
Hashing 154 => 154
Hashing 155 => 155
Hashing 156 => 156
Hashing 157 => 157
Hashing 158 => 158
Hashing 159 => 159
Hashing 160 => 151
Hashing 161 => 152
Hashing 162 => 153
Hashing 163 => 154
Hashing 164 => 155
Hashing 165 => 156
Hashing 166 => 157
Hashing 167 => 158
Hashing 168 => 159
Hashing 169 => 160
Hashing 170 => 152
Hashing 171 => 153
Hashing 172 => 154
Hashing 173 => 155
Hashing 174 => 156
Hashing 175 => 157
Hashing 176 => 158
Hashing 177 => 159
Hashing 178 => 160
Hashing 179 => 161
Hashing 180 => 153
Hashing 181 => 154
Hashing 182 => 155
Hashing 183 => 156
Hashing 184 => 157
Hashing 185 => 158
Hashing 186 => 159
Hashing 187 => 160
Hashing 188 => 161
Hashing 189 => 162
Hashing 190 => 154
Hashing 191 => 155
Hashing 192 => 156
Hashing 193 => 157
Hashing 194 => 158
Hashing 195 => 159
Hashing 196 => 160
Hashing 197 => 161
Hashing 198 => 162
Hashing 199 => 163
Hashing 200 => 146
Hashing 201 => 147
Hashing 202 => 148
Hashing 203 => 149
Hashing 204 => 150
Hashing 205 => 151
Hashing 206 => 152
Hashing 207 => 153
Hashing 208 => 154
Hashing 209 => 155
Hashing 210 => 147
Hashing 211 => 148
Hashing 212 => 149
Hashing 213 => 150
Hashing 214 => 151
Hashing 215 => 152
Hashing 216 => 153
Hashing 217 => 154
Hashing 218 => 155
Hashing 219 => 156
Hashing 220 => 148
Hashing 221 => 149
Hashing 222 => 150
Hashing 223 => 151
Hashing 224 => 152
Hashing 225 => 153
Hashing 226 => 154
Hashing 227 => 155
Hashing 228 => 156
Hashing 229 => 157
Hashing 230 => 149
Hashing 231 => 150
Hashing 232 => 151
Hashing 233 => 152
Hashing 234 => 153
Hashing 235 => 154
Hashing 236 => 155
Hashing 237 => 156
Hashing 238 => 157
Hashing 239 => 158
Hashing 240 => 150
Hashing 241 => 151
Hashing 242 => 152
Hashing 243 => 153
Hashing 244 => 154
Hashing 245 => 155
Hashing 246 => 156
Hashing 247 => 157
Hashing 248 => 158
Hashing 249 => 159
Hashing 250 => 151
Hashing 251 => 152
Hashing 252 => 153
Hashing 253 => 154
Hashing 254 => 155
Hashing 255 => 156

请注意数字趋势增加的倾向。这就是我们的陷阱。对于每个元素,运行哈希4次($hash = ourHash($hash)`),得到以下结果:
Hashing 0 => 153
Hashing 1 => 154
Hashing 2 => 155
Hashing 3 => 156
Hashing 4 => 157
Hashing 5 => 158
Hashing 6 => 150
Hashing 7 => 151
Hashing 8 => 152
Hashing 9 => 153
Hashing 10 => 157
Hashing 11 => 158
Hashing 12 => 150
Hashing 13 => 154
Hashing 14 => 155
Hashing 15 => 156
Hashing 16 => 157
Hashing 17 => 158
Hashing 18 => 150
Hashing 19 => 151
Hashing 20 => 158
Hashing 21 => 150
Hashing 22 => 154
Hashing 23 => 155
Hashing 24 => 156
Hashing 25 => 157
Hashing 26 => 158
Hashing 27 => 150
Hashing 28 => 151
Hashing 29 => 152
Hashing 30 => 150
Hashing 31 => 154
Hashing 32 => 155
Hashing 33 => 156
Hashing 34 => 157
Hashing 35 => 158
Hashing 36 => 150
Hashing 37 => 151
Hashing 38 => 152
Hashing 39 => 153
Hashing 40 => 154
Hashing 41 => 155
Hashing 42 => 156
Hashing 43 => 157
Hashing 44 => 158
Hashing 45 => 150
Hashing 46 => 151
Hashing 47 => 152
Hashing 48 => 153
Hashing 49 => 154
Hashing 50 => 155
Hashing 51 => 156
Hashing 52 => 157
Hashing 53 => 158
Hashing 54 => 150
Hashing 55 => 151
Hashing 56 => 152
Hashing 57 => 153
Hashing 58 => 154
Hashing 59 => 155
Hashing 60 => 156
Hashing 61 => 157
Hashing 62 => 158
Hashing 63 => 150
Hashing 64 => 151
Hashing 65 => 152
Hashing 66 => 153
Hashing 67 => 154
Hashing 68 => 155
Hashing 69 => 156
Hashing 70 => 157
Hashing 71 => 158
Hashing 72 => 150
Hashing 73 => 151
Hashing 74 => 152
Hashing 75 => 153
Hashing 76 => 154
Hashing 77 => 155
Hashing 78 => 156
Hashing 79 => 157
Hashing 80 => 158
Hashing 81 => 150
Hashing 82 => 151
Hashing 83 => 152
Hashing 84 => 153
Hashing 85 => 154
Hashing 86 => 155
Hashing 87 => 156
Hashing 88 => 157
Hashing 89 => 158
Hashing 90 => 150
Hashing 91 => 151
Hashing 92 => 152
Hashing 93 => 153
Hashing 94 => 154
Hashing 95 => 155
Hashing 96 => 156
Hashing 97 => 157
Hashing 98 => 158
Hashing 99 => 150
Hashing 100 => 154
Hashing 101 => 155
Hashing 102 => 156
Hashing 103 => 157
Hashing 104 => 158
Hashing 105 => 150
Hashing 106 => 151
Hashing 107 => 152
Hashing 108 => 153
Hashing 109 => 154
Hashing 110 => 155
Hashing 111 => 156
Hashing 112 => 157
Hashing 113 => 158
Hashing 114 => 150
Hashing 115 => 151
Hashing 116 => 152
Hashing 117 => 153
Hashing 118 => 154
Hashing 119 => 155
Hashing 120 => 156
Hashing 121 => 157
Hashing 122 => 158
Hashing 123 => 150
Hashing 124 => 151
Hashing 125 => 152
Hashing 126 => 153
Hashing 127 => 154
Hashing 128 => 155
Hashing 129 => 156
Hashing 130 => 157
Hashing 131 => 158
Hashing 132 => 150
Hashing 133 => 151
Hashing 134 => 152
Hashing 135 => 153
Hashing 136 => 154
Hashing 137 => 155
Hashing 138 => 156
Hashing 139 => 157
Hashing 140 => 158
Hashing 141 => 150
Hashing 142 => 151
Hashing 143 => 152
Hashing 144 => 153
Hashing 145 => 154
Hashing 146 => 155
Hashing 147 => 156
Hashing 148 => 157
Hashing 149 => 158
Hashing 150 => 150
Hashing 151 => 151
Hashing 152 => 152
Hashing 153 => 153
Hashing 154 => 154
Hashing 155 => 155
Hashing 156 => 156
Hashing 157 => 157
Hashing 158 => 158
Hashing 159 => 159
Hashing 160 => 151
Hashing 161 => 152
Hashing 162 => 153
Hashing 163 => 154
Hashing 164 => 155
Hashing 165 => 156
Hashing 166 => 157
Hashing 167 => 158
Hashing 168 => 159
Hashing 169 => 151
Hashing 170 => 152
Hashing 171 => 153
Hashing 172 => 154
Hashing 173 => 155
Hashing 174 => 156
Hashing 175 => 157
Hashing 176 => 158
Hashing 177 => 159
Hashing 178 => 151
Hashing 179 => 152
Hashing 180 => 153
Hashing 181 => 154
Hashing 182 => 155
Hashing 183 => 156
Hashing 184 => 157
Hashing 185 => 158
Hashing 186 => 159
Hashing 187 => 151
Hashing 188 => 152
Hashing 189 => 153
Hashing 190 => 154
Hashing 191 => 155
Hashing 192 => 156
Hashing 193 => 157
Hashing 194 => 158
Hashing 195 => 159
Hashing 196 => 151
Hashing 197 => 152
Hashing 198 => 153
Hashing 199 => 154
Hashing 200 => 155
Hashing 201 => 156
Hashing 202 => 157
Hashing 203 => 158
Hashing 204 => 150
Hashing 205 => 151
Hashing 206 => 152
Hashing 207 => 153
Hashing 208 => 154
Hashing 209 => 155
Hashing 210 => 156
Hashing 211 => 157
Hashing 212 => 158
Hashing 213 => 150
Hashing 214 => 151
Hashing 215 => 152
Hashing 216 => 153
Hashing 217 => 154
Hashing 218 => 155
Hashing 219 => 156
Hashing 220 => 157
Hashing 221 => 158
Hashing 222 => 150
Hashing 223 => 151
Hashing 224 => 152
Hashing 225 => 153
Hashing 226 => 154
Hashing 227 => 155
Hashing 228 => 156
Hashing 229 => 157
Hashing 230 => 158
Hashing 231 => 150
Hashing 232 => 151
Hashing 233 => 152
Hashing 234 => 153
Hashing 235 => 154
Hashing 236 => 155
Hashing 237 => 156
Hashing 238 => 157
Hashing 239 => 158
Hashing 240 => 150
Hashing 241 => 151
Hashing 242 => 152
Hashing 243 => 153
Hashing 244 => 154
Hashing 245 => 155
Hashing 246 => 156
Hashing 247 => 157
Hashing 248 => 158
Hashing 249 => 159
Hashing 250 => 151
Hashing 251 => 152
Hashing 252 => 153
Hashing 253 => 154
Hashing 254 => 155
Hashing 255 => 156

我们已经将自己缩小到了8个价值观......这很糟糕......我们的原始函数将S(∞)映射到S(256)。也就是说,我们创建了一个满射函数$input映射到$output
由于我们有一个满射函数,我们无法保证任何输入子集的映射不会发生冲突(实际上,在实践中它们会发生)。
这就是这里发生的事情!我们的函数很糟糕,但这不是为什么它起作用(这就是为什么它如此快速和完整地工作)。
同样的事情也发生在MD5中。它将S(∞)映射到S(2^128)。由于没有保证运行MD5(S(output))将是单射函数,也就是说它不会有冲突。

TL/DR部分

因此,直接将输出反馈给md5会产生碰撞,每次迭代都会增加碰撞的概率。然而,这是线性增长的,这意味着虽然 2 ^ 128 的结果集被减少了,但它并没有足够快地减少到成为一个关键缺陷。因此,...
$output = md5($input); // 2^128 possibilities
$output = md5($output); // < 2^128 possibilities
$output = md5($output); // < 2^128 possibilities
$output = md5($output); // < 2^128 possibilities
$output = md5($output); // < 2^128 possibilities

迭代次数越多,减少的程度就越大。

解决方法

幸运的是,我们有一个简单的方法来解决这个问题:将某些东西反馈到进一步的迭代中:

$output = md5($input); // 2^128 possibilities
$output = md5($input . $output); // 2^128 possibilities
$output = md5($input . $output); // 2^128 possibilities
$output = md5($input . $output); // 2^128 possibilities
$output = md5($input . $output); // 2^128 possibilities    

请注意,进一步的迭代对于每个单独的$input值并不是2^128。这意味着我们可能会生成$input值,它们仍然在后面发生碰撞(因此将在远少于2^128个可能输出处定居或共振)。但是对于$input的一般情况仍然像单个轮回那样强大。
等等,是吗?让我们使用我们的ourHash()函数进行测试。切换到$hash = ourHash($input . $hash);,进行100次迭代:
Hashing 0 => 201
Hashing 1 => 212
Hashing 2 => 199
Hashing 3 => 201
Hashing 4 => 203
Hashing 5 => 205
Hashing 6 => 207
Hashing 7 => 209
Hashing 8 => 211
Hashing 9 => 204
Hashing 10 => 251
Hashing 11 => 147
Hashing 12 => 251
Hashing 13 => 148
Hashing 14 => 253
Hashing 15 => 0
Hashing 16 => 1
Hashing 17 => 2
Hashing 18 => 161
Hashing 19 => 163
Hashing 20 => 147
Hashing 21 => 251
Hashing 22 => 148
Hashing 23 => 253
Hashing 24 => 0
Hashing 25 => 1
Hashing 26 => 2
Hashing 27 => 161
Hashing 28 => 163
Hashing 29 => 8
Hashing 30 => 251
Hashing 31 => 148
Hashing 32 => 253
Hashing 33 => 0
Hashing 34 => 1
Hashing 35 => 2
Hashing 36 => 161
Hashing 37 => 163
Hashing 38 => 8
Hashing 39 => 4
Hashing 40 => 148
Hashing 41 => 253
Hashing 42 => 0
Hashing 43 => 1
Hashing 44 => 2
Hashing 45 => 161
Hashing 46 => 163
Hashing 47 => 8
Hashing 48 => 4
Hashing 49 => 9
Hashing 50 => 253
Hashing 51 => 0
Hashing 52 => 1
Hashing 53 => 2
Hashing 54 => 161
Hashing 55 => 163
Hashing 56 => 8
Hashing 57 => 4
Hashing 58 => 9
Hashing 59 => 11
Hashing 60 => 0
Hashing 61 => 1
Hashing 62 => 2
Hashing 63 => 161
Hashing 64 => 163
Hashing 65 => 8
Hashing 66 => 4
Hashing 67 => 9
Hashing 68 => 11
Hashing 69 => 4
Hashing 70 => 1
Hashing 71 => 2
Hashing 72 => 161
Hashing 73 => 163
Hashing 74 => 8
Hashing 75 => 4
Hashing 76 => 9
Hashing 77 => 11
Hashing 78 => 4
Hashing 79 => 3
Hashing 80 => 2
Hashing 81 => 161
Hashing 82 => 163
Hashing 83 => 8
Hashing 84 => 4
Hashing 85 => 9
Hashing 86 => 11
Hashing 87 => 4
Hashing 88 => 3
Hashing 89 => 17
Hashing 90 => 161
Hashing 91 => 163
Hashing 92 => 8
Hashing 93 => 4
Hashing 94 => 9
Hashing 95 => 11
Hashing 96 => 4
Hashing 97 => 3
Hashing 98 => 17
Hashing 99 => 13
Hashing 100 => 246
Hashing 101 => 248
Hashing 102 => 49
Hashing 103 => 44
Hashing 104 => 255
Hashing 105 => 198
Hashing 106 => 43
Hashing 107 => 51
Hashing 108 => 202
Hashing 109 => 2
Hashing 110 => 248
Hashing 111 => 49
Hashing 112 => 44
Hashing 113 => 255
Hashing 114 => 198
Hashing 115 => 43
Hashing 116 => 51
Hashing 117 => 202
Hashing 118 => 2
Hashing 119 => 51
Hashing 120 => 49
Hashing 121 => 44
Hashing 122 => 255
Hashing 123 => 198
Hashing 124 => 43
Hashing 125 => 51
Hashing 126 => 202
Hashing 127 => 2
Hashing 128 => 51
Hashing 129 => 53
Hashing 130 => 44
Hashing 131 => 255
Hashing 132 => 198
Hashing 133 => 43
Hashing 134 => 51
Hashing 135 => 202
Hashing 136 => 2
Hashing 137 => 51
Hashing 138 => 53
Hashing 139 => 55
Hashing 140 => 255
Hashing 141 => 198
Hashing 142 => 43
Hashing 143 => 51
Hashing 144 => 202
Hashing 145 => 2
Hashing 146 => 51
Hashing 147 => 53
Hashing 148 => 55
Hashing 149 => 58
Hashing 150 => 198
Hashing 151 => 43
Hashing 152 => 51
Hashing 153 => 202
Hashing 154 => 2
Hashing 155 => 51
Hashing 156 => 53
Hashing 157 => 55
Hashing 158 => 58
Hashing 159 => 0
Hashing 160 => 43
Hashing 161 => 51
Hashing 162 => 202
Hashing 163 => 2
Hashing 164 => 51
Hashing 165 => 53
Hashing 166 => 55
Hashing 167 => 58
Hashing 168 => 0
Hashing 169 => 209
Hashing 170 => 51
Hashing 171 => 202
Hashing 172 => 2
Hashing 173 => 51
Hashing 174 => 53
Hashing 175 => 55
Hashing 176 => 58
Hashing 177 => 0
Hashing 178 => 209
Hashing 179 => 216
Hashing 180 => 202
Hashing 181 => 2
Hashing 182 => 51
Hashing 183 => 53
Hashing 184 => 55
Hashing 185 => 58
Hashing 186 => 0
Hashing 187 => 209
Hashing 188 => 216
Hashing 189 => 219
Hashing 190 => 2
Hashing 191 => 51
Hashing 192 => 53
Hashing 193 => 55
Hashing 194 => 58
Hashing 195 => 0
Hashing 196 => 209
Hashing 197 => 216
Hashing 198 => 219
Hashing 199 => 220
Hashing 200 => 248
Hashing 201 => 49
Hashing 202 => 44
Hashing 203 => 255
Hashing 204 => 198
Hashing 205 => 43
Hashing 206 => 51
Hashing 207 => 202
Hashing 208 => 2
Hashing 209 => 51
Hashing 210 => 49
Hashing 211 => 44
Hashing 212 => 255
Hashing 213 => 198
Hashing 214 => 43
Hashing 215 => 51
Hashing 216 => 202
Hashing 217 => 2
Hashing 218 => 51
Hashing 219 => 53
Hashing 220 => 44
Hashing 221 => 255
Hashing 222 => 198
Hashing 223 => 43
Hashing 224 => 51
Hashing 225 => 202
Hashing 226 => 2
Hashing 227 => 51
Hashing 228 => 53
Hashing 229 => 55
Hashing 230 => 255
Hashing 231 => 198
Hashing 232 => 43
Hashing 233 => 51
Hashing 234 => 202
Hashing 235 => 2
Hashing 236 => 51
Hashing 237 => 53
Hashing 238 => 55
Hashing 239 => 58
Hashing 240 => 198
Hashing 241 => 43
Hashing 242 => 51
Hashing 243 => 202
Hashing 244 => 2
Hashing 245 => 51
Hashing 246 => 53
Hashing 247 => 55
Hashing 248 => 58
Hashing 249 => 0
Hashing 250 => 43
Hashing 251 => 51
Hashing 252 => 202
Hashing 253 => 2
Hashing 254 => 51
Hashing 255 => 53

我这里看到了一个粗略的模式,但需要注意的是,它并不比我们的基础函数更具有模式性(基础函数本身已经非常弱了)。

然而请注意,即使在单次运行中没有冲突,03也会成为冲突。这是我之前所说的应用(即所有输入的冲突抵抗力保持不变,但由于基础算法中的缺陷,可能会出现特定的冲突路线)。

TL/DR 部分

通过将输入反馈到每个迭代中,我们有效地消除了在先前迭代中可能发生的任何冲突。

因此,md5($input . md5($input)); 理论上至少和 md5($input) 一样强。

这重要吗?

是的。这是 PBKDF2 取代 PBKDF1 的原因之一,详见 RFC 2898。考虑两者的内部循环:

PBKDF1:

T_1 = Hash (P || S) ,
T_2 = Hash (T_1) ,
...
T_c = Hash (T_{c-1}) 

其中c是迭代次数,P是密码,S是盐值。

PBKDF2:

U_1 = PRF (P, S || INT (i)) ,
U_2 = PRF (P, U_1) ,
...
U_c = PRF (P, U_{c-1})

在这里,PRF实际上只是一个HMAC。但是为了我们的目的,让我们说PRF(P,S) = Hash(P || S)(也就是说,两个输入的PRF大致上与将它们连接在一起的哈希相同)。它非常不是,但对于我们的目的而言,它是。
因此,PBKDF2保持了底层哈希函数的碰撞抗性,而PBKDF1则没有。
将所有内容绑定在一起:
我们知道安全迭代哈希的方法。事实上:
$hash = $input;
$i = 10000;
do {
   $hash = hash($input . $hash);
} while ($i-- > 0);

通常是安全的。

现在,为什么我们需要哈希呢?让我们分析一下熵的变化。

哈希函数接受无限集合:S(∞)并生成一个更小、大小一致的集合S(n)。下一次迭代(假设输入被传回)再次将S(∞)映射到S(n)

S(∞) -> S(n)
S(∞) -> S(n)
S(∞) -> S(n)
S(∞) -> S(n)
S(∞) -> S(n)
S(∞) -> S(n)

请注意,最终输出的熵与第一个完全相同。迭代不会使其“更加模糊”。熵是相同的。没有魔法来源的不可预测性(它是伪随机函数,而不是随机函数)。
然而,迭代确实有所收益。它使哈希过程人为地变慢。这就是为什么迭代可能是个好主意的原因。事实上,这是大多数现代密码哈希算法的基本原则(重复做某事会使它变慢)。
慢是好的,因为它可以对抗主要的安全威胁:暴力破解。我们使哈希算法越慢,攻击者就越难攻击从我们那里窃取的密码哈希。这是一件好事!!!

1
$output = md5($output); // < 2^128 possibilities --- 这里真的是严格的 <,还是 <= - zerkms
3
@zerkms:它不是严格的任何东西。我们需要了解底层函数(在本例中为 md5())的一些非常具体的细节,才能确切地知道。但一般来说,它将是 < 而不是 <=...请记住,我们正在谈论所有可能 $inputs$output 集合的大小。因此,如果我们有甚至 一个 碰撞,那么它将是 <,因此 < 是更好的泛化方式。 - ircmaxell
2
@TomášFejfar 我认为问题不是关于一般的碰撞,而是关于严格输出集合(2^128个输出,每个输出恰好128位宽)中的碰撞。那可能是可逆的,但据我所知,通用证明是不可能的(只有特定算法的碰撞示例证明)。考虑哈希函数,如果输入为128位,则简单地返回输入(否则进行哈希)。一般来说,它应该是满射的,但当输入其输出时,它总是可逆的...这就是争议的焦点... - ircmaxell
3
让我们在聊天中继续这个讨论:http://chat.stackoverflow.com/rooms/66609/discussion-between-ircmaxell-and-dan。 - ircmaxell
8
那些不想花时间去查看Dan和ircmaxell之间讨论如何结束的人们,可以放心了:讨论结束得很好,Dan同意了ircmaxell的观点。 - jeromej
显示剩余9条评论

54

是的,重新散列可以减少搜索空间,但不,这并不重要 - 有效的减少量微不足道。

重新散列会增加暴力破解所需的时间,但只进行两次也不是最佳选择。

你真正需要做的是使用PBKDF2来对密码进行散列 - 这是一种使用安全散列和盐以及迭代的经过验证的方法。查看此SO响应

编辑: 我差点忘了 - 不要使用MD5!!!使用现代密码散列函数,例如SHA-2系列(SHA-256、SHA-384和SHA-512)。


2
@DFTR - 同意。bcrypt或scrypt是更好的选择。 - orip
不要使用那些(SHA-2系列)它们现在也可以很容易地被破解,可以查看https://crackstation.net/进行证明。如果有需要,请使用Scrypt或PBKDF2,它们是基于密钥派生函数(KDFs)的加密散列函数。 - theodore hogberg
3
2016年,人们应该努力使用Argon2和scrypt。 - silkfire

10

是的 - 它减少了与字符串匹配可能的字符串数量。

正如您已经提到的,加盐哈希要好得多。

这里的一篇文章:http://websecurity.ro/blog/2007/11/02/md5md5-vs-md5/,试图证明为什么它是等效的,但我对这个逻辑不确定。部分原因是他们假设没有软件可以分析md5(md5(text)),但显然很容易生成彩虹表。

我仍然坚持我的答案是,md5(md5(text))类型的哈希比md5(text)哈希数更小,增加了碰撞的机会(即使仍然是不太可能的概率),并减少了搜索空间。


5

大多数回答都来自没有密码学或安全背景的人,他们是错误的。如果可能的话,使用盐,每个记录都是唯一的。MD5/SHA等算法速度太快,与您想要的相反。PBKDF2和bcrypt较慢(这很好),但可以通过ASICs/FPGA/GPUs(现在非常实惠)被打败。因此需要一种内存硬算法:输入scrypt

这里有一个关于盐和速度的通俗解释(但不包括内存硬算法)。


4
我只是从实际的角度来看待这个问题。黑客想要什么?答案是一组字符,通过散列函数后生成所需的哈希值。
你只保存了最后一个哈希值,因此黑客只需要暴力破解一个哈希值。假设每次暴力破解的成功率相同,那么哈希值的数量就无关紧要了。即使你进行了一百万次哈希迭代,也不会增加或减少任何安全性,因为在最后,还是只有一个哈希值需要破解,而破解的几率与任何哈希值相同。
也许之前的评论者认为输入很重要,但实际上并不是。只要你输入的内容能够生成所需的哈希值,不管是正确的输入还是错误的输入,都可以通过验证。
现在,彩虹表又是另一回事了。由于彩虹表只包含原始密码,因此两次哈希可能是一个好的安全措施,因为包含每个哈希值的彩虹表会变得太大。
当然,我只考虑了OP给出的例子,其中只是将明文密码进行哈希。如果你在哈希中包括用户名或盐值,那么两次哈希就完全没有必要了,因为包含正确哈希的彩虹表已经太大而不实用。
总之,我不是安全专家,但这是我从我的经验中得出的结论。

这个答案在各方面都是错误的。
  1. 知道倒数第二个哈希对攻击者没有任何价值,因为迭代哈希的输入是密码,然后进行多次哈希(而不是一次)。
  2. 输入空间是密码,输出空间是哈希密码。_典型_密码空间比输出空间小得多。
  3. 未加盐的双重哈希密码的彩虹表与未加盐的单重哈希密码的彩虹表一样大。
  4. 用户名熵低,好的盐是随机的。
  5. 盐不能替代迭代。你需要两者都有。
- Clement Cherlin

4
总的来说,对于相同的内容进行双重哈希或双重加密并不能提供额外的安全性。如果你能破解一次哈希,那么你也可以再次破解它。但通常这样做不会损害安全性。
在使用MD5的例子中,你可能知道有一些碰撞问题。"双重哈希"并不能真正帮助解决这个问题,因为相同的碰撞仍然会导致相同的第一个哈希值,然后你可以再次使用MD5获得第二个哈希值。
这确实可以防范字典攻击,比如那些“反向MD5数据库”,但盐值也可以达到同样的目的。
顺便说一下,双重加密并不能提供任何额外的安全性,因为它只是产生一个不同的键,这个键是两个实际使用的密钥的组合。所以寻找“键”的工作并没有加倍,因为实际上并不需要找到两个密钥。但对于哈希来说,结果通常与原始输入的长度不同。

1
所有正确,但我只想指出,MD5强碰撞抵抗力受损的影响被夸大了一点 - 大多数使用加密哈希函数的场景并不依赖于强碰撞抵抗力,只需要弱抵抗力。他们不会受到此漏洞的影响。 - SquareCog

3

据我所知,重新哈希密码数百或数千次实际上可能是推荐的。

这个想法是,如果你可以让编码密码的时间更长,那么攻击者要运行很多猜测来破解密码就需要更多的工作量。这似乎是重新哈希的优点——不是更加安全,而是生成字典攻击需要更长的时间。

当然,计算机变得越来越快,因此这种优势随着时间的推移会减弱(或需要增加迭代次数)。


我之前在另一条评论中也提到了这个,但是http://en.wikipedia.org/wiki/Key_stretching - user41871

2
假设您使用哈希算法:计算rot13,取前10个字符。如果您这样做两次(甚至2000次),可能会制作一个更快的函数,但其结果相同(即只需取前10个字符)。
同样,可能可以制作一个更快的函数,其输出与重复哈希函数相同。因此,您选择的哈希函数非常重要:就像rot13示例一样,不一定会通过重复哈希来提高安全性。如果没有研究表明该算法是为递归使用而设计的,则更安全的做法是假定它不会为您提供额外的保护。
话虽如此:对于除了最简单的哈希函数之外的所有哈希函数,最有可能需要密码学专家来计算更快的函数,因此,如果您正在防范无法访问密码学专家的攻击者,实际上使用重复哈希函数可能更安全。

2

个人而言,我不会费心去使用多个哈希值,但是我会确保除了密码之外,也要对用户名(或其他用户ID字段)进行哈希处理,以防止两个具有相同密码的用户最终得到相同的哈希值。此外,为了保险起见,我可能还会将一些其他常量字符串添加到输入字符串中。

$hashed_password = md5( "xxx" + "|" + user_name + "|" + plaintext_password);

13
应该为每个用户随机生成一个字符串,而不是使用常量。 - Bill the Lizard
8
如果您按建议添加用户名,那么一个不变的密钥可以起作用(且更易处理)。这本质上会产生一个随机的基于用户的密钥。 - SquareCog
4
一个不变的秘密盐是通过隐晦性来提高安全性。如果"秘密"泄露出去,即使用"xxx" + 用户名 + 密码,攻击者甚至不需要从你的表中获取数据就可以对其发起攻击。 - Bill the Lizard
8
我认为这并不是通过模糊来保证安全性。使用盐的原因是,你不能对多个MD5哈希同时计算彩虹表。针对"xxx"+密码(相同的盐)构建彩虹表只需要一次。而构建"xxx"+用户名+密码的彩虹表比暴力破解还要困难。 - FryGuy
5
“攻击被简化为构建一个针对特定用户名的字典”只是一种暴力攻击(实际上更糟,因为除了计算所有哈希值外,您还必须存储它们),所以在这种情况下盐起到了完美的作用。 - Kornel
显示剩余6条评论

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