Back

密码安全备忘录

本文是对 OWASP 文章的翻译,介绍了密码存储相关的密码学知识,包括常用的 Argon2、scrypt、bcrypt 等密码摘要算法,还根据实际情况总结出了最佳实践。

本文使用 CC BY SA 3.0 授权

关于 OWASP

维基百科:OWASP(中) | Wikipedia: OWASP(英)

OWASP(Open Web Application Security Project)是 Web 应用安全领域的在线社区,产出了许多免费的、知识共享许可的文章,也开发了许多自由开源的安全工具。

本文是 OWASP 备忘单系列的一部分,原文可见《Password Storage Cheat Sheet》。

密码安全很有必要,纵使数据库泄漏,也应该避免攻击者获取明文密码。大多数现代编程语言和框架,都提供内置的密码存储功能,以保证密码安全。

在攻击者获取了摘要过的密码后,他们能够在本地离线破解。作为防御者,只有通过选择尽可能消耗资源的摘要算法,才可能减缓离线攻击。

此备忘录提供了与存储密码有关的、需要考虑的各个方面的指南。简而言之:

  • 首选使用 Argon2id,最低参数为:15 MiB 的内存,2 次循环,1 的并行度。
  • 如果 Argon2id 不可用,则使用 scrypt,最低参数为:\(2^{16}\) 的 CPU 及内存花费,8(1024 字节)的块大小,1 的并行度。
  • 对于使用 bcrypt 的老系统,计算负载至少应为 10,密码也应限制在 72 字节长度以下。
  • 如果需要符合 FIPS-140 标准,请使用计算负载至少为 310,000 的 PBKDF2,内部摘要算法应使用 HMAC-SHA-256

背景

摘要和加密

摘要(hash,也称哈希、散列)和加密(encryption)都是保护敏感数据的方法。但在大多数情况下,密码应被摘要,而非被加密。

摘要是单向函数,也就是说,你不可能「解密」出原文。摘要函数正适合密码验证。纵使攻击者获得了摘要后的密码,他们也无力解密出原文,并登入受害者的帐号。

加密是双向函数,这意味着原文可以被取回。加密更适合存储地址这类数据,因为这些数据会以明文在前台显示。对地址摘要只会产生无意义的字符串。

在存储密码时,只有一些边缘案例才应使用加密。如果应用需要用密码与另一个不支持以编程方式鉴权的系统(如 OIDC,OpenID Connect)交互,使用加密才是必要的。如果可能,应该通过替换此类架构来避免以加密形式存储密码。

关于加密,请参见:加密存储备忘录(英)

攻击者如何破解密码摘要

尽管「解密」摘要是不可能的,但在某些情况下「破解」密码是有可能的。

基本步骤为:

  • 选择一个受害者可能使用的密码(如 password1!
  • 计算摘要
  • 将计算所得的摘要和受害者密码的摘要比较,若两者一致,则「破解」了该摘要,并取得了密码明文。

可以对大量潜在密码重复这一过程。获取潜在密码的方式包括:

  • 使用其他网站泄漏的密码
  • 暴力破解,即尝试每一个组合
  • 常用密码的字典或词表

虽然需要尝试的次数可能很多,但在高速硬件(如GPU)和服务器集群中,攻击者破解密码的成本相对较小,尤其是在没有遵循密码存储的最佳实践时。

使用现代摘要算法存储的强密码,并遵循最佳实践,攻击者实际上不可能破解。作为一个应用程序的所有者,你有责任选择一个现代的摘要算法。

密码存储概念

盐(salt)是一个独特的、随机生成的字符串,作为摘要过程的一部分被添加到每个密码中。盐对每个用户都是唯一的,攻击者使用每个盐来逐一破解摘要值,而不是计算一次摘要值就与密码比较。这使得暴力破解变得非常困难,因为需要的时间与摘要值的数量成正比。

加盐还可以防止攻击者使用彩虹表预先计算摘要值。最后,加盐还意味着,如果不破解摘要值,就不可能确定两个用户的密码是否相同。因为即使密码相同,不同的盐也会导致不同的摘要值。

胡椒

除了加盐之外,还可以使用胡椒(pepper)来提供额外的保护。胡椒的目的是为了防止攻击者在只有数据库访问权的情况下破解任何摘要值,例如,如果攻击者利用 SQL 注入或获得了数据库的备份。

其中一种加胡椒策略是,像往常一样对密码进行摘要,然后用对称的加密密钥对摘要进行HMAC或加密,再将密码摘要存储在数据库中,而密钥即为胡椒。胡椒策略不会影响密码摘要。

  • 胡椒在密码间共享,而非像盐,对每个密码不同。
  • 不像盐,胡椒不应存储在数据库
  • 胡椒应该存储在「保险箱」或 HSM(Hardware Security Modules,硬件安全模块)中。
  • 类似其他密钥,胡椒应该考虑旋转策略,即定期替换。

计算负载

计算负载(work factors),基本上是密码摘要算法的迭代次数(通常为 \(2^n\))。该参数旨在令计算摘要值更加昂贵,这降低了攻击者试图破解密码摘要值的速度,增加了破解成本。计算负载通常会存储在摘要输出中。

选择计算负载时,需要在安全性和性能之间取得平衡。较高的计算负载将使攻击者更难破解摘要值,但也会使验证登录尝试的过程变慢。如果计算负载太高,可能会降低应用程序的性能,也可能被攻击者用来 DDoS 以耗尽服务器资源。

计算负载没有黄金公式——取决于服务器性能和用户数量。为确定计算负载,应在服务器上测试。作为一般规则,计算时长应少于一秒。

升级计算负载

计算负载变量的一个关键优势就在于可以随着时间升级,因为硬件也会随着时间而变得更强大、更便宜。

常见的升级方法是,在用户下一次登录时用新参数重新摘要他们的密码。不过这种方法也有弊端,如果用户永远没有登录,则可能计算负载永远都不会升级。根据需求不同,直接删除旧摘要,并要求在下次登录时重置密码,也是可行的做法。

密码摘要算法

有许多现代摘要算法是专门为安全存储密码而设计的。它们应该很慢(不像 MD5 和 SHA-1 等算法,它们被设计得很快),并且可以通过更改计算负载来配置它们有多慢。

网站无需隐藏正在使用的密码摘要算法。如果使用现代密码摘要算法,且适当配置参数,公开声明是安全的。

下面列出了主要应考虑的三种算法。

Argon2id

Argon2(Wikipedia - 英) 是 2015 年 PHC(Password Hasing Competition,密码摘要竞赛)的获胜者。该算法有三个不同的版本,若无特别需求,应使用 Argon2id 变体,因为它同时抵抗侧信道和基于 GPU 的攻击。

Argon2id 的计算负载参数不像其他算法那样简单,它提供了几种不同的参数。以下配置之一是最低基准,包括了最低内存大小 \(s\)、最低循环次数 \(t\) 以及并行度 \(p\)

  • \(m=\pu{46MiB},\ t=1,\ p=1\)(不要和 Argon2i 一起使用)
  • \(m=\pu{19MiB},\ t=2,\ p=1\)(不要和 Argon2i 一起使用)
  • \(m=\pu{12MiB},\ t=3,\ p=1\)
  • \(m=\pu{9MiB},\ t=4,\ p=1\)
  • \(m=\pu{7MiB},\ t=5,\ p=1\)

这几个配置在防御方面是等效的。只有 CPU 和 RAM 开销间的权衡。

scrypt

scrypt(论文 - tarsnap.com - PDF)是由 Colin Percival 创建的基于密码的密钥迭代函数。尽管新系统应该考虑使用 Argon2id,但老系统在使用 scrypt 时也应恰当配置。

类似 Argon2id,scrypt 有三个不同的参数。以下配置之一是最低基准,包括了最低 CPU、内存开销参数 \(N\)、块大小 \(r\) 以及并行度 \(p\)

  • \(N=2^{16}=\pu{64 MiB},\ r=8=\pu{1024 B},\ p= 1\)
  • \(N=2^{15}=\pu{32 MiB},\ r=8=\pu{1024 B},\ p= 2\)
  • \(N=2^{14}=\pu{16 MiB},\ r=8=\pu{1024 B},\ p= 4\)
  • \(N=2^{13}=\pu{8 MiB},\ r=8=\pu{1024 B},\ p= 8\)
  • \(N=2^{12}=\pu{4 MiB},\ r=8=\pu{1024 B},\ p=15\)

这些配置在防御方面是等效的。只有 CPU 和 RAM 开销间的权衡。

bcrypt

只有在 argon2idscrypt 都不可用时,或需要 PBKDF2 以兼容 FIPS-140 时,才应该考虑 bcrypt,它是次优选择。

计算负载应在服务器性能允许的范围内尽可能大,最低为 10。

输入限制

对于大多数实现bcrypt 限制输入为至多 72 字节。为了避免问题,应当施加最多 72 字节长度的上限。

预摘要密码

另一种方法是用快速算法,如 SHA-256 对用户提供的密码预摘要,然后用 bcrypt 对得到的摘要再进行一轮摘要(即 bcrypt(base64(hmac-sha256(data:$password, key:$pepper)), $salt, $cost))。这是一种危险但常见的做法。但在bcrypt 与其他摘要函数相结合时,会出现密码洗牌和其他问题,应该避免这种做法。

PBKDF2

PBKDF2NIST 推荐,并且有 FIPS-140 验证的实现方式。因此,如果需要 FIPS-140 兼容,则应使用该算法。

PBKDF2 要求你选择一种内部摘要算法,如 HMAC 或其他各种摘要算法。HMAC-SHA-256 被广泛支持,并被 NIST 推荐。

PBKDF2 的计算负载是通过迭代次数实现的,应该根据内部摘要算法设置。

  • PBKDF2-HMAC-SHA1: 720,000 次迭代
  • PBKDF2-HMAC-SHA256: 310,000 次迭代
  • PBKDF2-HMAC-SHA512: 120,000 次迭代

这些配置在防御方面是等效的。

当 PBKDF2 与 HMAC 一起使用时,如果密码长于摘要函数的块大小(SHA-25664 字节),密码将被自动预摘要。例如,密码「This is a password longer than 512 bits which is the block size of SHA-256」(这是一个长于 SHA-256 块大小,512 比特以上的密码)被转换为摘要值:

fa91498c139805af73f7ba275cca071e78d78675027000c99a9925e2ec92eedd

一个好的 PBKDF2 的实现会在迭代阶段之前执行该步骤,但有些实现会在每次迭代时都转换。这可能使长密码摘要比短密码摘要要昂贵得多。如果用户可以提供非常长的密码,就会出现潜在的拒绝服务漏洞,比如 2013 年在 Django 中披露的漏洞。手动预摘要可以减少这种风险,但需要在预摘要步骤中添加盐。

升级老摘要

对于使用不太安全的摘要算法(如 MD5SHA-1)构建的旧应用程序,应升级为上述的现代密码摘要算法。当用户下次输入密码时,应该使用新的算法重新摘要。此外,令用户的当前密码过期,并要求输入一个新的密码,这样任何旧的(不太安全的)密码摘要就对攻击者不再有用了,也是一个好的做法。

然而,这种方法意味着旧的(不太安全的)密码摘要值将被保存在数据库中,直到用户登录。可以采取两种主要方法来避免这种困境。

一种方法是将长期不活动的用户的密码摘要值过期并删除,要求他们重新设置密码才能再次登录。虽然安全,但这种方法并不特别方便用户。过期许多用户的密码可能会给支持人员带来问题,或者也会被用户解释为漏洞。

另一种方法是使用现有的密码摘要值作为一个更安全的算法的输入。例如,如果应用程序最初以 md5($password) 的形式存储密码,这可以很容易地升级为 bcrypt(md5($password))。嵌套摘要避免了索要密码明文;然而,它可以使摘要值更容易被破解。这些摘要值应该在用户下次登录时,用密码明文的直接摘要值(bcrypt($password))来代替。

假设无论选择什么密码摘要方法,将来都必须进行升级。应该确保升级你的摘要算法时尽可能简单。对于过渡期,允许新旧摘要算法的混合使用。如果摘要值和工作负载一起存储,例如使用 PHC 字符串格式,那么混合使用摘要算法会更容易。

国际字符

确保你的摘要库能够接受广泛的字符,并与所有 Unicode 码位兼容。用户应使用现代设备上的全部字符,特别是移动键盘。他们应该能够从各种语言中选择密码并包括象形文字。在进行摘要之前,用户输入的熵不应该被降低。密码摘要库应当能处理输入包括 NULL(\0)字节的情况。

comments powered by Disqus