在存储之前对密码进行两次哈希处理的安全性是否比仅仅哈希一次更安全?
我在说什么是这样做的:
$hashed_password = hash(hash($plaintext_password));
而不仅仅是这个:
$hashed_password = hash($plaintext_password);
如果它不太安全,你能提供一个很好的解释(或链接到一个)吗?
此外,使用的哈希函数是否有所作为?如果混合使用md5和sha1(例如)而不是重复相同的散列函数,它会有什么不同吗?
注1:当我说"double hashing"我'm talking about hashing a password twice in an attempt to make it more obscured. I'我不是在谈论technique for resolving collisions .
Note 2: I know I need to add a random salt to really make it secure. The question is whether hashing twice with the same algorithm helps or hurts the hash.
16 回答
哈希密码一次是不安全的
不,多个哈希不是那么安全;它们是安全密码使用的重要组成部分 .
迭代哈希会增加攻击者在候选列表中尝试每个密码所需的时间 . 您可以轻松地将密码攻击所需的时间从数小时增加到数年 .
简单的迭代是不够的
仅将哈希输出链接到输入不足以实现安全性 . 迭代应该在保留密码熵的算法的上下文中进行 . 幸运的是,有几种已发布的算法已经受到足够的审查,可以对其设计充满信心 .
一个好的密钥派生算法,如PBKDF2,将密码注入每轮散列,减轻了对散列输出中的冲突的担忧 . PBKDF2可以按原样用于密码验证 . Bcrypt通过加密步骤跟随密钥推导;这样,如果发现了一种快速反转密钥派生的方法,攻击者仍然必须完成已知的明文攻击 .
如何破解密码
存储的密码需要防止脱机攻击 . 如果没有使用密码,则可以使用预先计算的字典攻击(例如,使用彩虹表)来破解密码 . 否则,攻击者必须花时间为每个密码计算一个哈希,看看它是否与存储的哈希匹配 .
所有密码都不太可能 . 攻击者可能会详尽地搜索所有短密码,但他们知道,每增加一个角色,他们蛮力成功的机会就会急剧下降 . 相反,他们使用最可能的密码的有序列表 . 它们以“password123”开头,并进入不经常使用的密码 .
假设攻击者名单很长,有100亿候选人;假设桌面系统每秒可以计算100万个哈希值 . 如果只使用一次迭代,攻击者可以测试她的整个列表少于三个小时 . 但如果仅使用2000次迭代,则该时间将延长至近8个月 . 为了击败更复杂的攻击者 - 例如能够下载可以利用其GPU功能的程序的攻击者 - 你需要更多的迭代 .
多少钱够了?
要使用的迭代次数是安全性和用户体验之间的权衡 . 可供攻击者使用的专用硬件很便宜,但是it can still perform hundreds of millions of iterations per second.攻击者系统的性能决定了在多次迭代的情况下中断密码需要多长时间 . 但是您的应用程序不太可能使用这种专用硬件 . 在不加剧用户的情况下可以执行多少次迭代取决于您的系统 .
您可以让用户在身份验证期间等待额外的3/4秒左右 . 分析您的目标平台,并使用尽可能多的迭代 . 我测试过的平台(移动设备上的一个用户,或服务器平台上的许多用户)可以轻松地支持PBKDF2,迭代次数在60,000到120,000之间,或者成本系数为12或13的bcrypt .
更多背景
阅读PKCS#5,获取有关salt和迭代在散列中的作用的权威信息 . 即使PBKDF2用于从密码生成加密密钥,它也可以作为密码验证的单向散列 . bcrypt的每次迭代都比SHA-2哈希更昂贵,因此您可以使用更少的迭代,但想法是相同的 . 通过使用派生密钥加密一个众所周知的纯文本,Bcrypt也超越了大多数基于PBKDF2的解决方案 . 生成的密文与一些元数据一起存储为“哈希” . 但是,没有什么能阻止你用PBKDF2做同样的事情 .
以下是我就此主题撰写的其他答案:
Hashing passwords
Hashing passwords
Salt
Hiding salt
PBKDF2 versus bcrypt
Bcrypt
对于那些说它安全的人来说,他们是正确的 in general . "Double"哈希(或其逻辑扩展,迭代哈希函数)绝对安全 if done right ,针对特定问题 .
对于那些说它不安全的人来说,他们是正确的 in this case . 发布在 is 问题中的代码不安全 . 我们来谈谈原因:
我们关注的哈希函数有两个基本属性:
前映像电阻 - 给定一个散列
$h
,应该很难找到一条消息$m
这样$h === hash($m)
Second-Pre-Image Resistance - 给定一条消息
$m1
,应该很难找到不同的消息$m2
这样hash($m1) === hash($m2)
碰撞阻力 - 应该很难找到一对消息
($m1, $m2)
这样hash($m1) === hash($m2)
(注意这类似于Second-Pre-Image阻力,但不同之处在于攻击者可以控制这两个消息)......For the storage of passwords ,我们真正关心的是Pre-Image Resistance . 另外两个是没有实际意义的,因为
$m1
是用户's password we'试图保持安全 . 因此,如果攻击者已经拥有它,那么哈希就无法保护...免责声明
接下来的一切都基于这样一个前提,即我们关心的是Pre-Image Resistance . 散列函数的另外两个基本属性可能不会(并且通常不会)以相同的方式保持 . 所以这篇文章的结论是 only applicable when using hash functions for the storage of passwords. They are not applicable in general...
让我们开始吧
为了便于讨论,让我们发明一下我们自己的哈希函数:
现在,这个哈希函数的功能应该非常明显 . 它将输入的每个字符的ASCII值相加,然后将该结果的模数取为256 .
所以让我们测试一下:
现在,让我们看看如果我们在函数周围运行几次会发生什么:
那输出:
嗯,哇 . 我们已经产生了碰撞!让我们试着看看为什么:
这是散列每个可能的散列输出的字符串的输出:
注意到更高数字的趋势 . 结果证明是我们的死亡 . 运行哈希4次($ hash = ourHash($ hash)`,每个元素)最终给我们:
我们've narrowed ourselves down to 8 values... That' s bad ...我们的原始函数将
S(∞)
映射到S(256)
. 那就是我们创建了一个Surjective Function映射$input
到$output
.由于我们有一个Surjective函数,我们无法保证输入的任何子集的映射都不会发生冲突(事实上,实际上它们会发生冲突) .
这就是这里发生的事情!我们的功能很糟糕,但这不是为什么这样做的原因(这就是为什么它如此快速而如此完整) .
MD5
也发生了同样的事情 . 它将S(∞)
映射到S(2^128)
. 由于无法保证运行MD5(S(output))
将Injective,这意味着它不会发生冲突 .TL / DR部分
因此,由于直接将输出反馈到
md5
会产生碰撞,每次迭代都会增加碰撞的机会 . 然而,这是线性增加,这意味着虽然2^128
的结果集减少了,但它并没有显着降低到足以成为关键缺陷 .所以,
迭代的次数越多,减少的次数就越多 .
修复
对我们来说幸运的是,有一种简单的方法可以解决这个问题:在进一步的迭代中反馈一些东西:
请注意,
$input
的每个单独值的进一步迭代不是2 ^ 128 . 这意味着我们可能能够生成仍然在线下碰撞的$input
值(因此将在远小于2^128
可能的输出处稳定或共振) . 但$input
的一般情况仍然像单轮一样强劲 .等等,是吗?让我们用
ourHash()
函数测试一下 . 切换到$hash = ourHash($input . $hash);
,进行100次迭代:那里的模式不再是我们的基础功能(已经非常弱) .
但请注意
0
和3
成了碰撞,即使它们不是我之前所说的应用(碰撞阻力对于所有输入的集合保持相同,但是由于底层的缺陷,特定的碰撞路径可能会打开)算法) .TL / DR部分
通过将输入反馈到每次迭代中,我们有效地破坏了先前迭代中可能发生的任何冲突 .
因此,
md5($input . md5($input));
应该(理论上至少)与md5($input)
一样强 .这很重要吗?
是 . 这是PBKDF2取代RFC 2898中的PBKDF1的原因之一 . 考虑两个内部循环::
PBKDF1:
其中
c
是迭代计数,P
是密码,S
是盐PBKDF2:
PRF真的只是一个HMAC . 但是对于我们这里的目的,我们只是说
PRF(P, S) = Hash(P || S)
(也就是说,2个输入的PRF是相同的,粗略地说,就像两个连接在一起的哈希) . 这非常 not ,但就我们的目的而言 .因此,PBKDF2保持底层
Hash
函数的碰撞阻力,而PBKDF1则没有 .将所有这些结合在一起:
我们知道迭代哈希的安全方法 . 事实上:
通常是安全的 .
现在,进入 why 我们想要哈希它,让我们分析熵运动 .
哈希采用无限集:
S(∞)
并生成一个更小,一致大小的集S(n)
. 下一次迭代(假设输入被传回in)再次将S(∞)
映射到S(n)
:请注意,最终输出具有 exactly the same amount of entropy as the first one . 迭代将 not "make it more obscured" . 熵是相同的 . 那里有一个伪随机函数,而不是一个随机函数 .
然而,迭代有一个好处 . 它使散列过程人为地变慢 . 这就是为什么迭代可能是一个好主意 . 事实上,它是大多数现代密码散列算法的基本原则(事实上,做一些事情反复使它变慢) .
慢是好的,因为它正在对抗主要的安全威胁:暴力破解 . 我们制作哈希算法的速度越慢,攻击者就越难以攻击我们窃取的密码哈希值 . 这是一件好事!!!
是的,重新散列减少了搜索空间,但不是,无关紧要 - 有效减少是微不足道的 .
重新散列会增加蛮力所需的时间,但这样做只有两次也不是最理想的 .
你真正想要的是用PBKDF2散列密码 - 一种使用盐和迭代的安全散列的成熟方法 . 看看this SO response .
EDIT :我差点忘了 - DON'T USE MD5!!!! 使用现代加密哈希,例如SHA-2系列(SHA-256,SHA-384和SHA-512) .
是 - 它减少了与字符串匹配的可能字符串的数量 .
正如你已经提到的,盐渍哈希要好得多 .
这里有一篇文章:http://websecurity.ro/blog/2007/11/02/md5md5-vs-md5/,尝试证明它为什么是等价的,但是我可以用来分析md5(md5(文本)),但显然生成彩虹表是相当简单的 .
我仍然坚持我的答案,md5(md5(文本))类型哈希值比md5(文本)哈希值少,增加了碰撞的几率(即使仍然是不太可能的概率)并减少了搜索空间 .
关于减少搜索空间的问题在数学上是正确的,尽管搜索空间足够大以至于所有实际目的(假设你使用盐),在2 ^ 128 . 但是,由于我们正在谈论密码,根据我的背包计算,可能的16个字符的字符串(字母数字,上限,投入的几个符号)的数量大约为2 ^ 98 . 因此,搜索空间的感知减少并不是真正相关的 .
除此之外,从密码学的角度来看,确实没有区别 .
虽然有一个称为“哈希链”的加密原语 - 一种允许你做一些很酷的技巧的技术,比如在使用后公开签名密钥,而不牺牲系统的完整性 - 给出最小的时间同步,这个允许您干净地回避初始密钥分发的问题 . 基本上,你预先计算了大量的哈希哈希值 - h(h(h(h ....(h(k))...))),使用第n个值来签名,在设定的间隔后,你发送输出密钥,然后使用密钥(n-1)对其进行签名 . 收件人现在可以验证您是否已发送所有以前的邮件,并且没有人可以伪造您的签名,因为它已经过了有效的时间段 .
比尔建议的数十万次重新散列只是浪费你的cpu ..如果你担心人们打破128位,请使用更长的密钥 .
我只是从实际角度来看待这个问题 . 之后的黑客是什么?为什么,字符组合在通过哈希函数时会生成所需的哈希值 .
您只保存最后一个哈希值,因此,黑客只需要强制使用一个哈希值 . 假设你在每个暴力步骤中遇到所需散列的几率相同,那么散列的数量是无关紧要的 . 你可以进行一百万次哈希迭代,并且它不会增加或减少一点安全性,因为在该行的末尾仍然只有一个哈希要破解,并且破坏它的几率与任何哈希相同 .
也许以前的海报认为输入是相关的;不是 . 只要您放入哈希函数中的任何内容都会生成所需的哈希值,它就会让您通过,输入正确或输入错误 .
现在,彩虹表是另一个故事 . 由于彩虹表只携带原始密码,因此包含两次散列的彩虹表可能是一个很好的安全措施,因为包含每个散列的每个散列的彩虹表太大 .
当然,我只考虑OP提供的示例,其中只是一个纯文本密码被哈希 . 如果你在哈希中包含用户名或盐,这是一个不同的故事;散列两次是完全没有必要的,因为彩虹表已经太大而不实用并且包含正确的散列 .
无论如何,这里不是安全专家,但这正是我从经验中得到的结论 .
就个人而言,我确保't bother with multiple hashses, but I'确保 also hash the UserName (or another User ID field) as well as the password 所以两个拥有相同密码的用户赢得't end up with the same hash. Also I' d可能会将一些其他常量字符串输入到输入字符串中以获得良好的衡量标准 .
大多数答案都是没有加密或安全背景的人 . 他们错了 . 如果可能的话,每个记录使用盐 . MD5 / SHA /等太快了,与你想要的相反 . PBKDF2和bcrypt速度较慢(这是好的),但可以用ASIC / FPGA / GPU(现在非常合理)来解决 . 因此需要一个内存硬算法:enter scrypt .
这是关于盐和速度的layman explanation(但不是关于内存硬算法) .
通常,它不提供双重哈希或双重加密的额外安全性 . 如果你可以打破哈希一次,你可以再次打破它 . 但是,这样做通常不会损害安全性 .
在您使用MD5的示例中,您可能知道存在一些冲突问题 . “Double Hashing”并没有真正帮助防止这种情况发生,因为相同的冲突仍将导致相同的第一个哈希值,然后您可以再次使用MD5来获取第二个哈希值 .
这确实可以防止字典攻击,例如那些“反向MD5数据库”,但是腌制也是如此 .
在切线上,Double加密某些东西不提供任何额外的安全性,因为它所做的只是导致一个不同的键,它是实际使用的两个键的组合 . 因此,找到“密钥”的努力不会加倍,因为实际上不需要找到两个密钥 . 对于散列,情况并非如此,因为散列的结果通常与原始输入的长度不同 .
从我所读到的,它实际上可能是建议重新散列密码数百或数千次 .
这个想法是,如果你可以花费更多的时间对密码进行编码,那么攻击者通过许多猜测来破解密码的工作就更多了 . 这似乎是重新散列的优势 - 而不是它在加密方面更安全,但生成字典攻击只需要更长的时间 .
当然,计算机总是变得更快,所以这种优势随着时间的推移而减少(或者需要你增加迭代次数) .
正如本文中的一些回复所暗示的那样,在某些情况下,它可能会提高安全性,而其他情况则会明显伤害安全性 . 有一个更好的解决方案,肯定会提高安全性 . 而不是将计算哈希值的次数加倍,将盐的大小加倍,或者将哈希值中使用的位数加倍,或者两者都做!而不是SHA-245,跳到SHA-512 .
我们假设您使用散列算法:计算rot13,取前10个字符 . 如果你这样做了两次(甚至2000次),就可以制作一个更快的功能,但是它会产生相同的结果(即只需要前10个字符) .
同样地,可以产生更快的功能,其提供与重复的散列函数相同的输出 . 因此,您选择散列函数非常重要:与rot13示例一样,重复散列不会提高安全性 . 如果没有研究表明该算法是为递归使用而设计的,那么假设它不会给你额外的保护是更安全的 .
这就是说:除了最简单的散列函数之外,它最有可能需要加密专家来计算更快的函数,所以如果你要防止那些无法访问加密专家的攻击者,那么在实践中使用重复散列函数可能更安全 .
只有当我在客户端上散列密码,然后在服务器上保存该散列的散列(使用不同的盐)时,双散列才对我有意义 .
这样即使有人黑客入侵服务器(从而忽略SSL提供的安全性),他仍然无法获得明确的密码 .
是的,他将拥有破坏系统所需的数据,但他无法使用该数据来破坏用户拥有的外部帐户 . 众所周知,人们几乎可以使用相同的密码 .
他可以获得明确密码的唯一方法是在客户端安装keygen - 这不再是你的问题了 .
简而言之:
客户端上的第一个散列在'server breach'场景中保护您的用户 .
服务器上的第二次散列用于保护您的系统,如果有人 grab 您的数据库备份,那么他就无法使用这些密码连接到您的服务 .
双重哈希是丑陋的,因为攻击者很可能已经 Build 了一个表来提供大多数哈希值 . 更好的是为你的哈希加盐,并将哈希混合在一起 . 还有新的模式来“签署”哈希(基本上是盐腌),但是以更安全的方式 .
是的 .
绝对 do not 使用传统散列函数的多次迭代,如
md5(md5(md5(password)))
. 最好的情况是你的安全性会略有提高(像这样的方案几乎没有提供任何针对GPU攻击的保护;只需要管道它 . )在最坏的情况下,你应该明智地承担最坏的情况 .Do 使用密码已由有能力的密码学家设计为有效的密码哈希,并且能够抵抗暴力攻击和时空攻击 . 这些包括bcrypt,scrypt,以及在某些情况下PBKDF2 . 基于glibc SHA-256的哈希也是可以接受的 .
我打算走出去,说它在某些情况下会更安全......但是不要低估我!
从数学/加密的角度来看,它不太安全,因为我确信别人会给你一个比我更清楚的解释 .
However ,存在MD5哈希的大型数据库,它们更可能包含"password"文本而不是MD5 . 因此,通过双重散列,您将降低这些数据库的有效性 .
当然,如果你使用盐,那么这种优势(劣势?)就会消失 .