加盐密码哈希 - 正确做法






4.94/5 (233投票s)
通过哈希加盐来保护密码
文章和代码由 Taylor Hornby (Defuse Security) 撰写,首次发布于: http://crackstation.net/hashing-security.htm。此文章由 adriancs 于 2012 年 10 月 26 日转发至 [The Insider News],然后,adriancs 再次将其作为文章转发,考虑到这将触达更多受众。
引言
如果您是一名 Web 开发人员,您可能不得不创建一个用户账户系统。用户账户系统中最重要的方面是如何保护用户密码。用户账户数据库经常被黑客攻击,因此,如果您的网站被入侵,您绝对必须采取措施来保护您用户的密码。保护密码的最佳方法是采用 加盐密码哈希。本文将解释其原因。
关于如何正确进行密码哈希,存在许多相互冲突的观点和误解,这可能是由于网络上充斥着错误信息。密码哈希是那些看似简单但却被许多人错误处理的事情之一。我希望通过本文不仅解释正确的方法,而且说明为何应该这样做。
如果您因为某种原因错过了那个醒目的红色警告,请现在就去阅读。真的,本指南不是为了指导您编写自己的存储系统,而是为了解释为何应以某种方式存储密码。
您可以使用以下链接跳转到本文的不同章节。
1. 什么是密码哈希? | 2. 哈希是如何被破解的 | 3. 添加盐 |
4. 无效的哈希方法 | 5. 如何正确哈希 | 6. 常见问题解答 |
什么是密码哈希?
hash("hello") = 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824 hash("hbllo") = 58756879c05c68dfac9866712fad6a93f8146f337a69afe7dd238f3364946366 hash("waltz") = c0e81794384491161f1777c232bc6bd9ec38f616560b120fda8e90f383853542
哈希算法是单向函数。它们将任意长度的数据转换为固定长度的“指纹”,无法反向解析。它们还具有一个特性,即输入数据即使发生微小变化,产生的哈希也会完全不同(参见上面的示例)。这对于保护密码非常有益,因为我们希望以一种即使密码文件本身被泄露也能保护密码的形式存储密码,但同时,我们又需要能够验证用户密码的正确性。
在基于哈希的账户系统中,账户注册和认证的一般工作流程如下:
- 用户创建账户。
- 用户的密码被哈希并存储在数据库中。在任何时候,明文(未加密)密码都不会被写入硬盘。
- 当用户尝试登录时,他们输入的密码的哈希将与(从数据库中检索到的)其真实密码的哈希进行比较。
- 如果哈希匹配,用户将被授予访问权限。否则,系统将告知用户输入的登录凭据无效。
- 步骤 3 和 4 在每次有人尝试登录其账户时都会重复。
在步骤 4 中,切勿告知用户是用户名还是密码错误。始终显示通用的消息,如“用户名或密码无效”。这可以防止攻击者在不知道密码的情况下枚举有效的用户名。
需要注意的是,用于保护密码的哈希函数与您在数据结构课程中可能见过的哈希函数不同。用于实现哈希表等数据结构的哈希函数旨在快速而非安全。只有加密哈希函数才能用于实现密码哈希。SHA256、SHA512、RipeMD 和 WHIRLPOOL 等哈希函数是加密哈希函数。
很容易认为您只需将密码通过加密哈希函数即可使您的用户密码安全。这与事实相去甚远。有许多方法可以非常快速地从明文哈希中恢复密码。有几种易于实现的技巧可以使这些“攻击”效果大打折扣。为了说明这些技术的需求,请考虑这个网站。在首页上,您可以提交一个哈希列表进行破解,并在不到一秒的时间内获得结果。显然,仅仅哈希密码并不能满足我们的安全需求。
下一节将讨论一些用于破解明文密码哈希的常见攻击。
哈希是如何被破解的
-
字典攻击和暴力破解攻击
字典攻击
=====================
尝试 apple:失败
尝试 blueberry:失败
尝试 justinbeiber:失败
. . .
尝试 letmein:失败
尝试 s3cr3t:成功!暴力破解攻击
=====================
尝试 aaaa:失败
尝试 aaab:失败
尝试 aaac:失败
. . .
尝试 acdb:失败
尝试 acdc:成功!破解哈希最简单的方法是尝试猜测密码,哈希每次猜测,并检查猜测的哈希是否等于正在破解的哈希。如果哈希相等,则猜测就是密码。最常见的两种猜测密码的方法是字典攻击和暴力破解攻击。
字典攻击使用一个包含单词、短语、常用密码和其他可能用作密码的字符串的文件。文件中的每个单词都被哈希,其哈希与密码哈希进行比较。如果匹配,则该单词就是密码。这些字典文件是通过从大量文本甚至真实的密码数据库中提取单词来构建的。通常还会对字典文件进行进一步处理,例如将单词替换为其“leet speak”等效项(“hello”变成“h3110”),以使其更有效。
暴力破解攻击尝试尝试给定长度内的所有可能字符组合。这些攻击计算成本非常高,并且通常是单位处理器时间破解哈希效率最低的,但它们最终总会找到密码。密码应该足够长,以至于通过搜索所有可能的字符串来找到它需要花费太长时间,不值得这样做。
没有办法阻止字典攻击或暴力破解攻击。它们可以被削弱,但无法完全阻止。如果您的密码哈希系统是安全的,那么破解哈希的唯一方法将是对每个哈希运行字典攻击或暴力破解攻击。
-
查找表
搜索:5f4dcc3b5aa765d61d8327deb882cf99:找到:password5
搜索:6cbe615c106f422d23669b610b564800: 不在数据库中
搜索:630bf032efe4507f2c57b280995925a9:找到:letMEin12
搜索:386f43fab5d096a7a66d67c8f213e5ec:找到:mcd0nalds
搜索:d5ec75d5fe70d428685510fae36492d9:找到:p@ssw0rd!查找表是一种极其有效的方法,可以快速破解相同类型的许多哈希。基本思想是预计算密码字典中密码的哈希,并将它们及其对应的密码存储在查找表数据结构中。一个良好的查找表实现可以在每秒处理数百次哈希查找,即使它们包含数十亿个哈希。
如果您想更好地了解查找表的速度,请尝试使用 CrackStation 的免费哈希破解器破解以下 sha256 哈希。
c11083b4b0a7743af748c85d343dfee9fbb8b2576c05f3a7f0d632b0926aadfc
08eac03b80adc33dc7d8fbe44b7c7b05d3a2c511166bdb43fcb710b03ba919e7
e4ba5cbd251c98e6cd1c23f126a3b81d8d8328abc95387229850952b3ef9f904
5206b8b8a996cf5320cb12ca91c7b790fba9f030408efe83ebb83548dc3007bd -
反向查找表
搜索 users' hash list 中的 hash(apple)... : 匹配 [alice3, 0bob0, charles8]
搜索 users' hash list 中的 hash(blueberry)... : 匹配 [usr10101, timmy, john91]
搜索 users' hash list 中的 hash(letmein)... : 匹配 [wilson10, dragonslayerX, joe1984]
搜索 users' hash list 中的 hash(s3cr3t)... : 匹配 [bruce19, knuth1337, john87]
搜索 users' hash list 中的 hash(z@29hjja)... : 没有用户使用此密码此攻击允许攻击者同时对多个哈希应用字典攻击或暴力破解攻击,而无需预先计算查找表。
首先,攻击者创建一个查找表,该表将来自被泄露的用户账户数据库的每个密码哈希映射到拥有该哈希的用户列表。然后,攻击者哈希每个密码猜测,并使用查找表获取攻击者猜测的密码的用户列表。此攻击特别有效,因为许多用户拥有相同密码的情况很常见。
-
彩虹表
彩虹表是一种时间-内存权衡技术。它们类似于查找表,只是它们牺牲了哈希破解速度以减小查找表的大小。因为它们更小,所以可以在相同的空间中存储更多哈希的解决方案,从而使其更有效。可以破解任何长度不超过 8 个字符的密码 MD5 哈希的彩虹表存在。
接下来,我们将介绍一种称为加盐的技术,该技术使得无法使用查找表和彩虹表来破解哈希。
添加盐
hash("hello") = 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824 hash("hello" + "QxLUF1bgIAdeQX") = 9e209040c863f84a31e719795b2577523954739fe5ed3b58a75cff2127075ed1 hash("hello" + "bv5PehSMfV11Cd") = d1d3ec2e6f20fd420d50e2642992841d8338a314b8ea157c9e18477aaef226ab hash("hello" + "YYLmfY6IehjZMQ") = a49670c3c18b9e079b9cfaf51634f563dc8ae3070db2c4a8544305df1b60f007
查找表和彩虹表之所以有效,是因为每个密码都以完全相同的方式进行哈希。如果两个用户具有相同的密码,他们将具有相同的密码哈希。通过随机化每个哈希,我们可以防止这些攻击,这样当同一个密码哈希两次时,哈希就不会相同。
我们可以通过在哈希之前将一个随机字符串(称为盐)附加或前置到密码来随机化哈希。如上面的示例所示,这使得相同的密码每次都会哈希成一个完全不同的字符串。为了检查密码是否正确,我们需要盐,因此它通常与哈希一起存储在用户账户数据库中,或者作为哈希字符串本身的一部分。
盐不需要保密。仅仅通过随机化哈希,查找表、反向查找表和彩虹表就会失效。攻击者不会提前知道盐是什么,因此他们无法预先计算查找表或彩虹表。如果每个用户的密码都使用不同的盐进行哈希,那么反向查找表攻击也将不起作用。
在下一节中,我们将讨论盐通常是如何被错误实现的。
错误的做法:短盐 & 重复使用盐
最常见的盐实现错误是重复使用相同的盐进行多次哈希,或者使用太短的盐。
重复使用盐
一个常见的错误是在每个哈希中使用相同的盐。盐要么被硬编码到程序中,要么被随机生成一次。这是无效的,因为如果两个用户具有相同的密码,他们仍然会拥有相同的哈希。攻击者仍然可以使用反向查找表攻击同时对每个哈希运行字典攻击。他们只需要将盐应用于每个密码猜测,然后再对其进行哈希。如果盐被硬编码到流行的产品中,那么就可以为该盐构建查找表和彩虹表,从而更容易破解由该产品生成的哈希。
每次用户创建账户或更改密码时,都必须生成一个新的随机盐。
短盐
如果盐太短,攻击者就可以为每种可能的盐构建一个查找表。例如,如果盐只有三个 ASCII 字符,那么只有 95x95x95 = 857,375 种可能的盐。这可能听起来很多,但如果每个查找表只包含 1MB 的最常用密码,总共也只有 837GB,考虑到如今 1000GB 的硬盘不到 100 美元就可以买到,这并不算多。
出于同样的原因,不应将用户名用作盐。用户名对单个服务可能是唯一的,但它们是可预测的,并且经常在其他服务的帐户上重复使用。攻击者可以为常用用户名构建查找表,并使用它们来破解用户名加盐哈希。
为了使攻击者无法为每种可能的盐创建查找表,盐必须足够长。一个好的经验法则是使用与哈希函数的输出大小相同的盐。例如,SHA256 的输出是 256 位(32 字节),因此盐应至少为 32 个随机字节。
错误的做法:双重哈希 & 奇怪的哈希函数
本节涵盖了另一个常见的密码哈希误解:奇怪的哈希算法组合。很容易过于投入而尝试组合不同的哈希函数,希望结果会更安全。然而,实际上,这样做的好处很小。它只会导致互操作性问题,有时甚至会降低哈希的安全性。切勿尝试发明自己的加密算法,始终使用由专家设计的标准。有人会争辩说,使用多个哈希函数会使计算哈希的过程变慢,因此破解速度也变慢,但正如我们稍后将看到的,有一种更好的方法可以使破解过程变慢。
以下是一些我在互联网论坛上看到的、被建议作为糟糕的奇怪哈希函数的例子。
- md5(sha1(password))
- md5(md5(salt) + md5(password))
- sha1(sha1(password))
- sha1(str_rot13(password + salt))
- md5(sha1(md5(md5(password) + sha1(password)) + md5(password)))
不要使用任何一个。
注意:本节已被证明是有争议的。我收到了不少电子邮件,认为奇怪的哈希函数是件好事,因为它能让攻击者不知道在使用哪个哈希函数,攻击者不太可能预先计算出奇怪哈希函数的彩虹表,并且计算哈希函数需要更长时间。
攻击者无法攻击他不知道算法的哈希,但请注意 Kerckhoffs 原理,攻击者通常可以访问源代码(特别是如果是免费或开源软件),并且鉴于目标系统中的一些密码-哈希对,反向工程算法并不困难。计算奇怪的哈希函数确实需要更长时间,但只是一个很小的常数因子。最好使用一种旨在极难并行化的迭代算法(下面将讨论这些)。而且,正确地给哈希加盐可以解决彩虹表问题。
如果您确实想使用标准的“奇怪”哈希函数,如 HMAC,那也可以。但如果您这样做的原因是使哈希计算变慢,请先阅读下面的关于密钥拉伸的部分。
将这些微小的好处与意外实现完全不安全的哈希函数和奇怪哈希引起的互操作性问题的风险进行比较。显然,最好使用标准且经过充分测试的算法。
哈希碰撞
由于哈希函数将任意长度的数据映射到固定长度的字符串,因此必然存在一些输入会哈希成相同的字符串。加密哈希函数旨在使这些碰撞极其难以找到。不时地,密码学家会发现对哈希函数的“攻击”,使查找碰撞更容易。最近的一个例子是 MD5 哈希函数,它确实发现了碰撞。
碰撞攻击是字符串(而不是用户密码)更有可能具有相同哈希的标志。然而,即使是像 MD5 这样较弱的哈希函数,找到碰撞也需要大量的专用计算能力,因此在实际中,这些碰撞“意外”发生的可能性非常小。使用 MD5 和盐哈希的密码,在所有实际用途中,其安全性与使用 SHA256 和盐哈希的密码一样。尽管如此,如果可能,最好使用更安全的哈希函数,如 SHA256、SHA512、RipeMD 或 WHIRLPOOL。
正确做法:如何正确哈希
本节将详细介绍密码应该如何哈希。第一小节涵盖了基础知识——所有绝对必要的内容。接下来的小节将解释如何增强基础知识,使哈希更难破解。
基础:加盐哈希
警告:不要只阅读本节。您绝对必须实现下一节的内容:“使密码破解更困难:慢哈希函数”。
我们已经看到了恶意黑客如何使用查找表和彩虹表快速破解明文哈希。我们了解到通过加盐来随机化哈希是解决这个问题的方法。但是,我们如何生成盐,以及如何将其应用于密码?
盐应该使用密码学安全伪随机数生成器 (CSPRNG) 来生成。CSPRNG 与普通伪随机数生成器(如“C”语言的rand()函数)有很大不同。顾名思义,CSPRNG 被设计为密码学安全的,这意味着它们提供高水平的随机性和完全不可预测性。我们不希望我们的盐是可以预测的,因此我们必须使用 CSPRNG。下表列出了一些存在于流行编程平台上的 CSPRNG。
平台 | CSPRNG |
---|---|
PHP | mcrypt_create_iv, openssl_random_pseudo_bytes |
Java | java.security.SecureRandom |
Dot NET (C#, VB) | System.Security.Cryptography.RNGCryptoServiceProvider |
Ruby | SecureRandom |
Python | os.urandom |
Perl | Math::Random::Secure |
C/C++ (Windows API) | CryptGenRandom |
GNU/Linux 或 Unix 上的任何语言 | 从/dev/random 或 /dev/urandom 读取 |
盐需要每个用户每个密码都是唯一的。每次用户创建账户或更改密码时,都应使用新的随机盐来哈希密码。切勿重复使用盐。盐还需要足够长,以至于存在许多可能的盐。一般来说,让您的盐至少与哈希函数的输出一样长。盐应与哈希一起存储在用户账户表中。
存储密码
- 使用 CSPRNG 生成一个长的随机盐。
- 将盐添加到密码前面,并使用标准密码哈希函数(如 Argon2、bcrypt、scrypt 或 PBKDF2)进行哈希。
- 将盐和哈希都保存在用户的数据库记录中。
验证密码
- 从数据库中检索用户的盐和哈希。
- 将盐添加到给定的密码前面,并使用相同的哈希函数进行哈希。
- 将给定密码的哈希与数据库中的哈希进行比较。如果它们匹配,则密码正确。否则,密码不正确。
在 Web 应用程序中,始终在服务器端进行哈希
如果您正在编写 Web 应用程序,您可能会想在哪里进行哈希。密码应该在用户浏览器中使用 JavaScript 进行哈希,还是应该“明文”发送到服务器并在那里进行哈希?
即使您在 JavaScript 中哈希用户的密码,您仍然必须在服务器端哈希哈希。考虑一个网站,它在用户浏览器中哈希用户的密码,而不在服务器端哈希哈希。为了验证用户,该网站将接受来自浏览器的哈希,并检查该哈希是否与数据库中的哈希完全匹配。这似乎比仅在服务器端哈希更安全,因为用户的密码永远不会发送到服务器,但事实并非如此。
问题在于,客户端哈希在逻辑上成为了用户的密码。用户要进行身份验证所需要做的就是告诉服务器其密码的哈希。如果坏人获得了用户的哈希,他们就可以使用它来向服务器进行身份验证,而无需知道用户的密码!因此,如果坏人以某种方式窃取了这个假设网站的哈希数据库,他们将立即获得对所有用户账户的访问权限,而无需猜测任何密码。
这并不是说您不应该在浏览器中进行哈希,但如果您这样做,您绝对必须在服务器端进行哈希。在浏览器中进行哈希当然是个好主意,但请考虑以下几点来实现:
-
客户端密码哈希不是 HTTPS(SSL/TLS)的替代品。如果浏览器和服务器之间的连接不安全,中间人就可以修改下载的 JavaScript 代码,从而删除哈希功能并获取用户的密码。
-
一些 Web 浏览器不支持 JavaScript,而一些用户会在其浏览器中禁用 JavaScript。因此,为了最大程度的兼容性,您的应用程序应检测浏览器是否支持 JavaScript,如果不支持,则在服务器端模拟客户端哈希。
-
您也需要为客户端哈希加盐。显而易见的方法是让客户端脚本向服务器请求用户的盐。不要这样做,因为它允许坏人无需知道密码即可检查用户名是否有效。由于您在服务器端也使用哈希和盐(使用良好的盐),因此可以使用用户名(或电子邮件)与特定于站点的字符串(例如域名)连接作为客户端盐。
使密码破解更困难:慢哈希函数
盐确保攻击者无法使用查找表和彩虹表等专用攻击快速破解大量哈希,但它并不能阻止他们对每个哈希单独运行字典攻击或暴力破解攻击。高端图形卡 (GPU) 和定制硬件每秒可以计算数十亿次哈希,因此这些攻击仍然非常有效。为了使这些攻击效果减弱,我们可以使用一种称为密钥拉伸的技术。
其思想是使哈希函数非常慢,这样即使使用快速 GPU 或定制硬件,字典攻击和暴力破解攻击也太慢以至于不值得。目标是使哈希函数足够慢以阻碍攻击,但又足够快以至于不会对用户造成明显的延迟。
密钥拉伸使用一种特殊的 CPU 密集型哈希函数来实现。不要尝试自己发明——简单地迭代哈希哈希是不够的,因为它可以在硬件中并行化并以与普通哈希相同的速度执行。使用标准算法,如PBKDF2 或bcrypt。您可以在这里找到 PBKDF2 的 PHP 实现:PBKDF2。
这些算法接受安全因子或迭代计数作为参数。此值决定了哈希函数的慢速程度。对于桌面软件或智能手机应用程序,选择此参数的最佳方法是在设备上运行简短的基准测试,以找到使哈希花费大约半秒钟的值。这样,您的程序就可以尽可能安全,而不会影响用户体验。
如果您在 Web 应用程序中使用密钥拉伸哈希,请注意,您将需要额外的计算资源来处理大量身份验证请求,并且密钥拉伸可能会使运行拒绝服务 (DoS) 攻击更容易。我仍然建议使用密钥拉伸,但使用较低的迭代计数。您应该根据您的计算资源和预期的最大身份验证请求速率来计算迭代计数。通过让用户每次登录时都解决 CAPTCHA,可以消除拒绝服务威胁。始终将您的系统设计成可以将来增加或减少迭代计数。
如果您担心计算负担,但仍想在 Web 应用程序中使用密钥拉伸,请考虑使用 JavaScript 在用户浏览器中运行密钥拉伸算法。Stanford JavaScript Crypto Library 包含 PBKDF2。迭代计数应设置得足够低,以便系统在使用较慢的客户端(如移动设备)时可用,并且系统应在用户浏览器不支持 JavaScript 时回退到服务器端计算。客户端密钥拉伸并不能消除服务器端哈希的必要性。您必须像哈希普通密码一样哈希客户端生成的哈希。
不可破解的哈希:密钥哈希和密码哈希硬件
只要攻击者可以使用哈希来检查密码猜测是否正确或错误,他们就可以运行字典攻击或暴力破解攻击。下一步是向哈希添加密钥,这样只有知道密钥的人才能使用哈希来验证密码。这可以通过两种方式实现。哈希可以使用 AES 等密码进行加密,或者可以使用 HMAC 等密钥哈希算法将密钥包含在哈希中。
这并不像听起来那么容易。即使在发生泄露的情况下,密钥也必须对攻击者保密。如果攻击者获得了系统的完全访问权限,他们无论将密钥存储在哪里都能窃取它。密钥必须存储在外部系统中,例如专门用于密码验证的物理上独立的服务器,或者附加到服务器的特殊硬件设备,例如YubiHSM。
我强烈推荐这种方法用于任何大规模(超过 100,000 个用户)的服务。我认为对于托管超过 1,000,000 个用户账户的任何服务来说,这都是必需的。
如果您负担不起多台专用服务器或特殊硬件设备,您仍然可以在标准 Web 服务器上获得密钥哈希的部分好处。大多数数据库是通过SQL 注入攻击被泄露的,在大多数情况下,这些攻击不会让攻击者访问本地文件系统(如果您的 SQL 服务器有此功能,请禁用本地文件系统访问)。如果您生成一个随机密钥并将其存储在一个无法从 Web 访问的文件中,并将其包含在加盐哈希中,那么如果您的数据库通过简单的 SQL 注入攻击被泄露,哈希就不会受到威胁。不要将密钥硬编码到源代码中,而是在应用程序安装时随机生成它。这不像使用单独的系统进行密码哈希那样安全,因为如果 Web 应用程序存在 SQL 注入漏洞,那么很可能存在其他漏洞,例如本地文件包含,攻击者可以利用这些漏洞读取密钥文件。但总比没有好。
请注意,密钥哈希不会取代盐的必要性。聪明的攻击者最终会找到方法来窃取密钥,因此重要的是哈希仍然受到盐和密钥拉伸的保护。
其他安全措施
密码哈希可以保护密码免遭安全泄露。它并不能使整个应用程序更安全。必须采取更多措施来防止密码哈希(和其他用户数据)被盗。
即使是经验丰富的开发人员也必须接受安全方面的教育才能编写安全的应用程序。学习 Web 应用程序漏洞的一个好资源是开放 Web 应用程序安全项目 (OWASP)。一个很好的入门是OWASP Top Ten 漏洞列表。除非您了解列表中的所有漏洞,否则不要尝试编写涉及敏感数据的 Web 应用程序。确保所有开发人员都接受了安全应用程序开发方面的充分培训是雇主的责任。
让第三方对您的应用程序进行“渗透测试”是个好主意。即使是最好的程序员也会犯错误,所以让安全专家审查代码是否存在潜在漏洞总是很有意义的。找到一个值得信赖的组织(或聘请员工)定期审查您的代码。安全审查过程应在应用程序的早期生命阶段开始,并贯穿其整个开发过程。
在发生安全事件时,监控您的网站以检测事件也很重要。我建议至少聘请一名全职负责检测和响应安全事件的人员。如果安全事件未被检测到,攻击者就可以让您的网站感染访问者恶意软件,因此及时检测和响应安全事件至关重要。
常见问题解答
我应该使用哪个哈希算法?
应该使用
- 精心设计的密钥拉伸算法,如PBKDF2、bcrypt 和scrypt。
- OpenWall 的便携式 PHP 密码哈希框架
- 我用 PHP、C#、Java 和 Ruby 实现的 PBKDF2。
- 安全的 crypt 版本 ($2y$、 $5$、$6$)
不要使用
- 快速的加密哈希函数,如 MD5、SHA1、SHA256、SHA512、RipeMD、WHIRLPOOL、SHA3 等。
- 不安全的 crypt 版本 ($1$、$2$、$2x$、$3$)。
- 任何您自己设计的算法。只使用已知的、由经验丰富的密码学家充分测试过的技术。
尽管 MD5 或 SHA1 没有加密攻击能够使其哈希更容易被破解,但它们已过时,并且被广泛(有些错误地)认为不适合密码存储。因此,我不建议使用它们。PBKDF2 是一个例外,它经常使用 SHA1 作为底层哈希函数来实现。
如果用户忘记了密码,我该如何让他们重置密码?
我个人认为,目前广泛使用的所有密码重置机制都不安全。如果您有高安全要求,例如加密服务,请不要让用户重置密码。
大多数网站使用电子邮件循环来验证忘记密码的用户。为此,请生成一个与账户强关联的、一次性使用的随机令牌。将其包含在发送到用户电子邮件地址的密码重置链接中。当用户单击包含有效令牌的密码重置链接时,提示他们输入新密码。确保令牌与用户账户强关联,这样攻击者就无法使用发送到其自己电子邮件地址的令牌来重置其他用户的密码。
令牌必须在 15 分钟内到期,或在使用后到期,以先到者为准。当用户登录(他们记住了密码)或请求另一个重置令牌时,使任何现有密码令牌失效也是个好主意。如果令牌不过期,它可能会永远被用来破解用户账户。电子邮件 (SMTP) 是一种明文协议,互联网上可能存在恶意路由器记录电子邮件流量。而且,用户账户(包括重置链接)可能在他们的密码更改后很长一段时间内被泄露。尽快使令牌到期可以减少用户暴露于这些攻击的风险。
攻击者将能够修改令牌,因此不要在令牌中存储用户账户信息或超时信息。它们应该是不可预测的随机二进制块,仅用于识别数据库表中的记录。
切勿通过电子邮件向用户发送新密码。请记住,在用户重置密码时,请选择一个新的随机盐。不要重复使用用于哈希其旧密码的盐。
如果我的用户账户数据库泄露/被黑,我该怎么办?
您的首要任务是确定系统是如何被攻破的,并修补攻击者使用的漏洞。如果您没有处理泄露的经验,我强烈建议聘请第三方安全公司。
您可能会想掩盖泄露事件,并希望没有人注意到。然而,试图掩盖泄露事件会让您看起来更糟,因为您没有告知用户他们的密码和其他个人信息可能已被泄露,从而将用户置于更大的风险之中。您必须尽快告知您的用户——即使您还不完全了解发生了什么。在您网站的首页上放置一个通知,链接到一个提供更详细信息的页面,并尽可能通过电子邮件向每位用户发送通知。
向您的用户解释他们的密码是如何受到保护的——希望是加盐哈希——并且即使它们受到加盐哈希的保护,恶意黑客仍然可以对哈希进行字典攻击和暴力破解攻击。恶意黑客会利用找到的任何密码尝试登录用户的其他网站账户,希望他们在两个网站上都使用了相同的密码。告知您的用户此风险,并建议他们更改他们在任何使用相似密码的网站或服务上的密码。强制他们在下次登录时更改您的服务密码。大多数用户会尝试将密码“更改”为原始密码,以快速绕过强制更改。使用当前密码哈希以确保他们不能这样做。
即使使用加盐慢哈希,攻击者很可能也能非常快速地破解一些弱密码。为了减少攻击者利用这些密码的机会窗口,您应该要求除了当前密码之外,还要进行电子邮件循环身份验证,直到用户更改密码。有关实现电子邮件循环身份验证的技巧,请参阅上一题,“如果用户忘记了密码,我该如何让他们重置密码?”
同时告知您的用户您的网站上存储了哪些类型的个人信息。如果您的数据库包含信用卡号,您应该指示您的用户仔细检查他们最近和未来的账单,并取消他们的信用卡。
我的密码策略应该是什么?我应该强制使用强密码吗?
如果您的服务没有严格的安全要求,那么就不要限制您的用户。我建议在用户输入密码时显示有关其密码强度的信息,让他们自己决定他们想要密码有多安全。如果您有特殊安全需求,请强制最小长度为 12 个字符,并要求至少包含两个字母、两个数字和两个符号。
不要强迫您的用户每六个月以上更改一次密码,因为这样做会产生“用户疲劳”,并使他们不太可能选择好密码。相反,请训练用户在感觉密码已被泄露时更改密码,并且永远不要将密码告诉任何人。如果是企业环境,鼓励员工利用带薪时间来记住和练习他们的密码。
如果攻击者能够访问我的数据库,他们不能只是用自己的哈希替换我的密码哈希然后登录吗?
是的,但是如果有人能够访问您的数据库,他们很可能已经能够访问您服务器上的一切了,所以他们不需要登录到您的账户来获取他们想要的东西。密码哈希的目的(在网站的上下文中)不是为了保护网站免遭泄露,而是为了在发生泄露时保护密码。
您可以通过使用具有不同权限的两个用户连接到数据库来防止在 SQL 注入攻击期间替换哈希。一个用于“创建账户”代码,另一个用于“登录”代码。“创建账户”代码应该能够读写用户表,而“登录”代码应该只能读取。
为什么我必须使用像 HMAC 这样的特殊算法?为什么不能将密码附加到密钥上?
MD5、SHA1 和 SHA2 等哈希函数使用 Merkle–Damgård 构造,这使得它们容易受到所谓的长度扩展攻击。这意味着,给定一个哈希 H(X),攻击者可以找到 H(pad(X) + Y) 的值,对于任何其他字符串 Y,而无需知道 X。pad(X) 是哈希使用的填充函数。
这意味着,给定一个哈希 H(key + message),攻击者可以计算 H(pad(key + message) + extension),而无需知道密钥。如果哈希被用作消息认证码,用于防止攻击者修改消息并用不同的有效哈希替换它,那么系统就会失败,因为攻击者现在拥有 message + extension 的有效哈希。
目前尚不清楚攻击者如何利用此攻击更快地破解密码哈希。然而,由于这种攻击,将普通哈希函数用于密钥哈希被认为是糟糕的做法。聪明的密码学家有一天可能会想出巧妙的方法来利用这些攻击来加快破解速度,所以请使用 HMAC。
盐应该在密码之前还是之后?
不重要,但选择一种并坚持下去以实现互操作性。盐在密码之前似乎更常见。
为什么此页面上的哈希代码以“固定长度”时间比较哈希值?
以“固定长度”时间比较哈希值可以确保攻击者无法通过计时攻击从在线系统中提取密码哈希,然后在离线破解它。
检查两个字节序列(字符串)是否相同的标准方法是比较第一个字节,然后是第二个,然后是第三个,依此类推。一旦找到一个字节对两个字符串不同,您就知道它们不同,并可以立即返回负响应。如果您在不找到任何不同字节的情况下通过了两个字符串,您就知道字符串是相同的,并可以返回正面结果。这意味着比较两个字符串所需的时间可能因字符串匹配的程度而异。
例如,对字符串“xyzabc”和“abcxyz”的标准比较会立即发现第一个字符不同,而不会检查字符串的其余部分。另一方面,当比较字符串“aaaaaaaaaaB”和“aaaaaaaaaaZ”时,比较算法会扫描“a”块,然后才能确定字符串不相等。
假设攻击者想闯入一个将身份验证尝试速率限制为每秒一次的在线系统。同时假设攻击者知道密码哈希的所有参数(盐、哈希类型等),除了哈希本身(显然)和密码。如果攻击者能够精确测量在线系统比较真实密码哈希与攻击者提供的密码哈希所需的时间,他就可以使用计时攻击提取哈希的一部分并对其进行离线破解,从而绕过系统的速率限制。
首先,攻击者找到 256 个哈希以每个可能的字节开头鐨字符串。他将每个字符串发送到在线系统,并记录系统响应所需的时间。花费时间最长的字符串将是其哈希的第一个字节与真实哈希的第一个字节匹配的字符串。攻击者现在知道了第一个字节,然后可以以类似的方式攻击第二个字节,然后是第三个字节,依此类推。一旦攻击者知道了足够的哈希,他就可以使用自己的硬件对其进行破解,而不会受到系统速率限制。
通过网络运行计时攻击似乎是不可能的。然而,它已经被完成,并且已被证明是可行的。这就是为什么此页面上的代码以一种无论字符串匹配多少都能花费相同时间的方式比较字符串。
SlowEquals 代码是如何工作的?
上一问题解释了为什么需要 SlowEquals,本问题解释了代码实际上是如何工作的。
private static boolean slowEquals(byte[] a, byte[] b) { int diff = a.length ^ b.length; for(int i = 0; i < a.length && i < b.length; i++) diff |= a[i] ^ b[i]; return diff == 0; }
代码使用 XOR "^" 运算符来比较整数是否相等,而不是 "==" 运算符。原因是如下所述的。当且仅当两个整数完全相同时,它们的 XOR 结果才会为零。这是因为 0 XOR 0 = 0, 1 XOR 1 = 0, 0 XOR 1 = 1, 1 XOR 0 = 1。如果我们将其应用于两个整数的所有位,则结果仅在所有位都匹配时才为零。
因此,在第一行,如果 a.length
等于 b.length
,则 diff 变量将获得零值,但如果不是,它将获得非零值。接下来,我们使用 XOR 比较字节,并将结果 OR 到 diff 中。如果字节不同,这将把 diff 设置为非零值。由于 OR 运算永远不会清除位,因此 diff 在循环结束时唯一为零的方式是它在循环开始之前就为零 (a.length == b.length) 并且两个数组中的所有字节都匹配(没有 XOR 产生非零值)。
我们需要使用 XOR 而不是 "==" 运算符来比较整数的原因是,“==”通常会被翻译/编译/解释为分支。例如,C 代码“diff &= a == b
”可能会编译为以下 x86 汇编
MOV EAX, [A] CMP [B], EAX JZ equal JMP done equal: AND [VALID], 1 done: AND [VALID], 0
分支会导致代码的执行时间根据整数的相等性和 CPU 的内部分支预测状态而变化。
C 代码“diff |= a ^ b
”应该编译为类似下面的内容,其执行时间不取决于整数的相等性
MOV EAX, [A] XOR EAX, [B] OR [DIFF], EAX
为什么还要哈希?
您的用户正在向您的网站输入密码。他们信任您来保护他们的安全。如果您的数据库被黑,并且您的用户密码未受保护,那么恶意黑客就可以使用这些密码来泄露您用户在其他网站和服务上的账户(大多数人到处使用相同的密码)。风险不仅仅是您自己的安全,还有您用户的安全。您对您用户的安全负有责任。