密码存储速查表¶
简介¶
本速查表就身份验证密码的正确存储方法提供建议。存储密码时,即使应用程序或数据库被攻破,也必须保护它们免受攻击者的侵害。幸运的是,大多数现代语言和框架都提供了内置功能来帮助安全地存储密码。
然而,一旦攻击者获取到存储的密码哈希,他们总是能够离线暴力破解哈希。防御者可以通过选择尽可能资源密集型的哈希算法来减缓离线攻击的速度。
我们的建议总结如下
- 使用Argon2id,最低配置为19 MiB内存、迭代计数2和并行度1。
- 如果Argon2id不可用,请使用scrypt,最低CPU/内存成本参数为(2^17),最小块大小为8(1024字节),并行化参数为1。
- 对于使用bcrypt的旧系统,请使用10或更高的工作因子,并限制密码长度为72字节。
- 如果需要符合FIPS-140标准,请使用PBKDF2,工作因子为600,000或更高,并将其内部哈希函数设置为HMAC-SHA-256。
- 考虑使用椒来提供额外的纵深防御(尽管单独使用,它不提供额外的安全特性)。
背景¶
哈希与加密¶
哈希和加密都可以保护敏感数据安全,但在几乎所有情况下,密码都应该被哈希,而不是加密。
因为哈希是一种单向函数(即,不可能“解密”哈希并获取原始明文值),它是最适合密码验证的方法。即使攻击者获取到哈希后的密码,他们也无法以受害者的身份登录。
由于加密是一种双向函数,攻击者可以从加密数据中检索原始明文。它可用于存储用户地址等数据,因为这些数据会在用户资料中以明文显示。哈希他们的地址会导致一堆乱码。
在密码中使用加密的唯一情况是,在需要获取原始明文密码的边缘情况下。如果应用程序需要使用密码与不支持现代程序化授权方式(如OpenID Connect (OIDC))的另一个系统进行身份验证,则可能需要这样做。在可能的情况下,应使用替代架构以避免以加密形式存储密码的需要。
有关加密的进一步指导,请参阅加密存储速查表。
密码哈希何时可能被破解¶
使用现代哈希算法并遵循哈希最佳实践存储的强密码,对于攻击者来说应是几乎不可能破解的。 作为应用程序所有者,您有责任选择一种现代哈希算法。
然而,在某些情况下,攻击者可以通过以下方式“破解”哈希
- 选择您认为受害者已选的密码(例如
password1!
) - 计算哈希
- 将您计算的哈希与受害者的哈希进行比较。如果匹配,您就成功“破解”了哈希,并知道了他们密码的明文值。
通常,攻击者会使用大量潜在候选密码列表重复此过程,例如
- 从其他受损网站获取的密码列表
- 暴力破解(尝试所有可能的候选)
- 常用密码的字典或词汇表
虽然排列组合的数量可能非常巨大,但凭借高速硬件(例如GPU)和可租用大量服务器的云服务,攻击者进行成功的密码破解的成本相对较小,尤其是在未遵循哈希最佳实践的情况下。
增强密码存储的方法¶
加盐¶
盐是一个唯一的、随机生成的字符串,在哈希过程中将其添加到每个密码中。由于每个用户的盐都是唯一的,攻击者必须使用相应的盐逐个破解哈希,而不是计算一次哈希然后与每个存储的哈希进行比较。这使得破解大量哈希变得更加困难,因为所需时间与哈希数量成正比增长。
加盐还可以防止攻击者使用彩虹表或基于数据库的查找进行预计算哈希。最后,加盐意味着即使密码相同,由于不同的盐会导致不同的哈希,因此在不破解哈希的情况下无法确定两个用户是否拥有相同的密码。
现代哈希算法,如Argon2id、bcrypt和PBKDF2,会自动对密码进行加盐处理,因此使用它们时无需额外步骤。
加椒¶
加椒是一类策略,可以在加盐的基础上使用,以提供额外的保护层。它防止攻击者在仅能访问数据库(例如,通过SQL注入漏洞或获取数据库备份)的情况下破解任何哈希。
加椒策略的常见要求¶
- 椒是在存储密码之间共享的,而不是像密码盐那样对单个密码来说是唯一的。
- 与密码盐不同,椒不应公开,并且不应与生成的哈希一起存储。椒应与密码数据库分开存储。
- 椒是秘密,应存储在“秘密保险库”或HSM(硬件安全模块)中。有关安全存储秘密的更多信息,请参阅秘密管理速查表。
- 如果椒遭到泄露,则必须更改椒。在不知道用户密码的情况下,无法更改椒。因此,更改椒将需要强制所有受旧椒保护其密码的用户重置其密码。
预哈希椒¶
在此策略中,在密码哈希算法对密码进行哈希处理之前,将椒添加到密码中。然后将计算出的哈希存储在数据库中。在这种情况下,椒应该是一个安全生成的随机值。有关安全生成随机值的更多信息,请参阅加密存储速查表。
后哈希椒¶
在此策略中,密码照常使用密码哈希算法进行哈希。然后使用HMAC(例如HMAC-SHA256、HMAC-SHA512,取决于所需的输出长度)再次对生成的密码哈希进行哈希,然后将最终哈希存储在数据库中。在这种情况下,椒充当HMAC密钥,应根据HMAC算法的要求生成。
使用工作因子¶
工作因子是为每个密码执行的哈希算法迭代次数(通常,实际上是2^work
次迭代)。工作因子通常存储在哈希输出中。它使哈希计算的计算成本更高,从而降低了攻击者尝试破解密码哈希的速度和/或增加了成本。
选择工作因子时,请在安全性与性能之间取得平衡。尽管较高的工作因子使攻击者更难破解哈希,但它们会减慢登录尝试的验证过程。如果工作因子过高,应用程序的性能可能会下降,攻击者可能会通过大量登录尝试耗尽服务器CPU,从而利用这一点进行拒绝服务攻击。
没有关于理想工作因子的黄金法则——它将取决于服务器的性能和应用程序上的用户数量。确定最佳工作因子需要对应用程序使用的特定服务器进行实验。一般而言,计算一个哈希应该花费不到一秒钟。
升级工作因子¶
工作因子的一个关键优势是,随着硬件变得更强大和更便宜,它可以随着时间的推移而增加。
升级工作因子最常见的方法是等待用户下次认证时,然后使用新的工作因子重新哈希他们的密码。不同的哈希将具有不同的工作因子,如果用户不重新登录应用程序,哈希可能永远不会被升级。根据应用程序的不同,可能适合删除旧的密码哈希,并要求用户在下次登录时重置密码,以避免存储旧的、安全性较低的哈希。
密码哈希算法¶
一些现代哈希算法专门设计用于安全存储密码。这意味着它们应该很慢(不像MD5和SHA-1等算法旨在快速),并且您可以通过更改工作因子来调整它们的慢速程度。
您不需要隐藏应用程序使用的密码哈希算法。如果您使用配置参数得当的现代密码哈希算法,则公开声明正在使用哪些密码哈希算法并将其列在此处应该是安全的。
应考虑的三种哈希算法
Argon2id¶
Argon2是2015年密码哈希竞赛的获胜者。在三种Argon2版本中,请使用Argon2id变体,因为它在抵御侧信道和基于GPU的攻击方面提供了平衡的方法。
与其他算法的简单工作因子不同,Argon2id有三个可配置的参数:最小内存大小的基本最小值(m)、最小迭代次数(t)和并行度(p)。我们推荐以下配置设置
- m=47104 (46 MiB), t=1, p=1 (不与Argon2i一起使用)
- m=19456 (19 MiB), t=2, p=1 (不与Argon2i一起使用)
- m=12288 (12 MiB), t=3, p=1
- m=9216 (9 MiB), t=4, p=1
- m=7168 (7 MiB), t=5, p=1
这些配置设置提供了同等的防御级别,唯一的区别在于CPU和RAM使用之间的权衡。
scrypt¶
scrypt是由Colin Percival创建的基于密码的密钥派生函数。虽然Argon2id应该是密码哈希的最佳选择,但当前者不可用时,应使用scrypt。
与Argon2id类似,scrypt有三个可配置的参数:最小CPU/内存成本参数(N)、块大小(r)和并行度(p)。请使用以下设置之一
- N=2^17 (128 MiB), r=8 (1024 字节), p=1
- N=2^16 (64 MiB), r=8 (1024 字节), p=2
- N=2^15 (32 MiB), r=8 (1024 字节), p=3
- N=2^14 (16 MiB), r=8 (1024 字节), p=5
- N=2^13 (8 MiB), r=8 (1024 字节), p=10
这些配置设置提供了同等的防御级别。唯一的区别在于CPU和RAM使用之间的权衡。
bcrypt¶
bcrypt密码哈希函数应仅用于Argon2和scrypt不可用的旧系统中进行密码存储。
工作因子应尽可能大,只要验证服务器性能允许,最低为10。
bcrypt的输入限制¶
bcrypt的输入最大长度在大多数实现中为72字节,因此您应强制执行最大密码长度为72字节(如果所使用的bcrypt实现限制更小,则应更少)。
使用bcrypt进行密码预哈希¶
另一种方法是使用SHA-2、HMAC或BLAKE3等快速算法对用户提供的密码进行预哈希处理,然后使用bcrypt对生成的哈希值进行哈希(即bcrypt(H($password)), $salt, $cost)
)。这可能是危险的,因为哈希输出值中可能存在空字节,并且可能导致密码剥离。
原始bcrypt期望一个以null结尾的密码字符串,这意味着哈希值将只使用到哈希值中的第一个null字节。(如果H($password)[0] == 0
,则bcrypt(H($password)), $salt, $cost) == bcrypt("", $salt, $cost)
)这增加了将bcrypt与其他哈希函数结合时发现冲突的机会,可以通过使用base64等方式将哈希值编码为可打印字符串来避免。base64可以将哈希值的长度增加到72个字符以上,因此对于SHA-512等哈希生成的大哈希值会有一些截断,但这可以忽略不计。
密码剥离利用了一个事实,即很容易检查bcrypt(base64(H($password))), $salt, $cost) == bcrypt(base64($leaked_hash), $salt, $cost)
是否成立。如果内部哈希函数H
在其他地方使用了相同的密码并且攻击者已知,那么破解密码就可以简化为破解哈希函数H
。仅仅使用纯SHA-512(即bcrypt(base64(sha512($password))), $salt, $cost)
)是一种危险的做法,其安全性与仅使用纯SHA-512相同。密码剥离仅在攻击者已知泄露的哈希(通过泄露的数据库或彩虹表)时才有效。为了缓解密码剥离,可以使用椒。
总结来说,如果必须使用bcrypt并且需要对密码进行预哈希,您应该执行bcrypt(base64(hmac-sha384(data:$password, key:$pepper)), $salt, $cost)
,并且不要将椒存储在数据库中。
PBKDF2¶
由于PBKDF2受到NIST的推荐,并且拥有FIPS-140验证的实现,因此当需要符合这些标准时,它应该是首选算法。
PBKDF2算法要求您选择一个内部哈希算法,例如HMAC或各种其他哈希算法。HMAC-SHA-256得到广泛支持并受到NIST的推荐。
PBKDF2的工作因子通过迭代计数实现,应根据所使用的内部哈希算法进行不同设置。
- PBKDF2-HMAC-SHA1: 1,300,000 次迭代
- PBKDF2-HMAC-SHA256: 600,000 次迭代
- PBKDF2-HMAC-SHA512: 210,000 次迭代
并行PBKDF2¶
- PPBKDF2-SHA512: 成本 2
- PPBKDF2-SHA256: 成本 5
- PPBKDF2-SHA1: 成本 10
这些配置设置在提供的防御方面是等效的。(截至2022年12月的数据,基于RTX 4000 GPU测试)
PBKDF2 预哈希¶
当PBKDF2与HMAC一起使用,并且密码长度超过哈希函数的块大小(SHA-256为64字节)时,密码将自动进行预哈希。例如,密码“This is a password longer than 512 bits which is the block size of SHA-256”将被转换为哈希值(十六进制):fa91498c139805af73f7ba275cca071e78d78675027000c99a9925e2ec92eedd
。
PBKDF2的良好实现会在昂贵的迭代哈希阶段之前执行预哈希。然而,一些实现会在每次迭代时执行转换,这会使哈希长密码的成本显著高于哈希短密码。当用户提供非常长的密码时,可能会出现潜在的拒绝服务漏洞,例如2013年Django中公布的漏洞。手动预哈希可以降低此风险,但需要在预哈希步骤中添加一个盐。
升级旧版哈希¶
使用MD5或SHA-1等安全性较低的哈希算法的旧应用程序,可以升级到上述现代密码哈希算法。当用户输入其密码(通常通过在应用程序上进行身份验证)时,应使用新算法重新哈希该输入。防御者应使用户的当前密码过期并要求他们输入新密码,以便其密码的任何旧版(安全性较低)哈希不再对攻击者有用。
然而,这意味着旧版(安全性较低)的密码哈希将一直存储在数据库中,直到用户登录。您可以采取以下两种方法之一来避免这种困境。
升级方法一:使长时间不活动用户的密码哈希过期并删除,并要求他们重置密码才能再次登录。尽管安全,但这种方法用户体验不佳。使许多用户的密码过期可能会给支持人员带来问题,或者被用户解读为泄露的迹象。
升级方法二:使用现有密码哈希作为更安全算法的输入。例如,如果应用程序最初将密码存储为md5($password)
,则可以轻松升级为bcrypt(md5($password))
。哈希分层避免了需要知道原始密码的问题;但是,它可能使哈希更容易被破解。下次用户登录时,应将这些哈希替换为用户密码的直接哈希。
请记住,一旦您选择了密码哈希方法,将来就必须对其进行升级,因此请确保升级哈希算法尽可能简单。在过渡期间,允许新旧哈希算法混合使用。如果密码哈希算法和工作因子与密码一起使用标准格式存储(例如模块化PHC字符串格式),则混合使用哈希算法会更容易。
国际字符¶
您的哈希库必须能够接受各种字符,并且应与所有Unicode码点兼容,以便用户可以使用现代设备(尤其是移动键盘)上可用的所有字符范围。他们应该能够选择来自各种语言的密码,并包含象形文字。在哈希之前,不应降低用户输入的熵,并且密码哈希库需要能够使用可能包含NULL字节的输入。