安全密码存储
在软件开发中,我们经常使用基于密码的用户认证,并且需要安全地存储用户密码:既要让用户能够注册、认证和修改密码,又要确保攻击者即使设法访问保存用户账户的数据库,也无法将存储的密码解密还原为明文值。
开发网站或 Web 应用时,通常会有一个需要使用用户名 + 密码进行登录后才能访问的管理面板。移动应用、Web 服务和其他受密码保护的系统也是如此:它们都需要安全密码存储(安全密码管理/强密码哈希存储)。
开发者通常像存储其他用户数据一样,将网站、应用或其他系统的用户密码存入数据库,但大多数系统都会应用某种哈希、加密或密码认证方案。实现基于密码认证的密码存储有很多方法,其中最常见的列于下表:
| 方法 | 安全性 | 说明 |
|---|---|---|
| 明文密码 | 极低 | 绝对不要这样做:服务器一旦遭到入侵,所有密码都会泄露 |
| 简单密码哈希 | 低 | 容易受到字典攻击 |
| 加盐密码哈希 | 中等 | 容易受到基于 GPU 和 ASIC 的密码破解 |
| 安全 KDF 函数(如 Argon2) | 高 | 推荐使用,并采用高强度 KDF 参数 |
下面回顾这些密码存储方法,并讨论它们的安全级别以及优缺点。
明文密码——绝对不要采用的反模式
最简单也最不安全的密码存储和基于密码的认证方法,是将明文密码直接写入数据库。
- 在这种场景下,为了检查密码,开发者只需将待检查密码与数据库中的密码进行比较。
- 绝对不要这样做!!!这是软件开发中的反模式,出于多种原因都非常糟糕。
- 管理员能够看到用户密码,这非常危险,因为许多用户会在多个网站/应用中使用相同密码,例如在 GMail、Facebook 和 Twitter 上使用同一个密码。
- 管理员绝不应知道用户密码,但应能在紧急情况下修改密码。
- 如果有人入侵服务器并访问数据库,就会以明文形式看到所有用户密码。
- 在任何信息系统/应用中保存明文密码都是非常糟糕的做法!
- 千万不要这样做!
简单密码哈希——高度不安全
一种相对简单但也相对不安全的密码存储和基于密码的认证方法,是使用 SHA-256(password) 之类的简单密码哈希,并将其直接写入数据库。
- 在这种场景下,为了检查密码,开发者只需将待检查密码的哈希与数据库中的密码哈希进行比较。
- 避免这样做!这是一种高度不安全的方法。
- 密码一旦被破解并以明文泄露,将造成真正的灾难,因为大多数用户会在多个网站/应用中使用相同密码。
加盐密码哈希——安全但仍不够
一种更复杂且相对安全的密码存储和基于密码的认证方法,是使用加盐密码哈希,并以 { salt + hash(password + salt) } 对的形式写入数据库。哈希函数可以是 SHA-256 等任何强密码学哈希。
- 其思路是:每次将密码写入数据库时,都保存不同的随机盐和随之变化的密码哈希值。因此,同一密码每次都会得到不同的 { salt + hash } 组合。
- 为了检查密码,开发者使用数据库中的盐对待检查密码计算哈希,再将计算出的哈希与数据库中的哈希进行比较。
- 这种方法可以很好地防止字典攻击,但无法防止基于 GPU 和 ASIC 的暴力密码破解攻击。
- 它还存在与使用 hash(key + msg) 代替 HMAC(key, msg) 相同的安全问题,例如长度扩展攻击。
- 总体而言,保存加盐密码哈希比前述方法更安全,但仍应避免使用。
- 应使用更能抵抗攻击、难以暴力破解的密码哈希函数代替简单哈希,例如 Argon2 或 Scrypt。
基于安全 KDF 的密码哈希——推荐
最复杂且最安全的密码存储和基于密码的认证方法,是使用基于安全 KDF 的密码哈希,并以 { salt + KDF(password, salt) } 对的形式写入数据库。密钥派生函数(KDF)应当强大且安全,例如使用精心选择参数的 Scrypt 或 Argon2。
- 其思路是:为每个密码哈希保存不同的随机盐,同时保存由 Scrypt 或 Argon2 等安全 KDF 函数(使用合理的迭代次数和 RAM 用量设置)派生出的密钥。
- 为了检查密码,从数据库中取出盐,使用与密码存入数据库时相同的 KDF 函数和 KDF 参数,从待检查密码中派生密钥。然后将派生密钥与数据库中的密钥进行比较。
- 这种方法能够抵抗大多数攻击,并被视为软件行业的标准做法。
- 它的安全性取决于 KDF 函数及所选 KDF 参数。例如,使用 128 MB RAM、4 个线程和 200 ms 计算时间的 Argon2 可能适合一般场景。
- 破解者无法有效执行暴力攻击,因为密码猜测速度会非常缓慢,即便使用配有优秀 CPU 和 GPU 的现代计算机,甚至使用专用 ASIC 硬件也是如此。
结论:使用 Argon2 和 Scrypt 等安全 KDF 函数在数据库中保存密码哈希。绝不要使用明文密码!
基于密码的认证
使用安全密码存储只是 Web 应用、移动应用和互联网服务实现安全的基于密码认证流程的一个组成部分。采用基于密码认证的系统会面临多种攻击:
- 密码猜测攻击:攻击者通过并行尝试大量登录来猜测/暴力破解用户密码。
- 一种简单的解决办法,是在每次错误登录尝试后设置逐渐增加的登录延迟(再次允许登录前的等待时间),甚至临时锁定账户。延迟/锁定应基于 IP 地址 + 用户名实施,以免影响合法用户登录。
- 安全的基于 KDF 的密码存储会延缓密码猜测过程,因此强烈推荐使用。
- 在登录失败 2-3 次后使用 CAPTCHA,可以提供很好的保护。
- 拒绝服务攻击:攻击者可能尝试登录过多次以使系统过载,或者针对同一用户进行过多次无效登录尝试,试图锁定其账户。
- 针对此攻击的防护措施与前一种攻击类似:使用 CAPTCHA,并在每次登录尝试后,针对特定 IP 地址延迟登录流程。
- 截获与重放攻击:攻击者可能截获认证通信(嗅探登录名/密码/认证票据/其他凭据),并在之后使用截获的凭据登录。
- 中间人攻击:攻击者可以截获并修改服务器与客户端之间的流量,诱骗用户泄露登录凭据。
- 可使用带服务器证书的 TLS 安全连接来解决此问题,该证书会认证服务器。
- 在某些场景(如网上银行)中,还会通过数字证书或 OTP(一次性密码)认证客户端。
- 服务器遭入侵攻击:如果认证服务器及其数据库遭到入侵,所有认证数据均被泄露,攻击者仍不应能够获取用户的明文密码。
- 首先必须明确:只要认证服务器遭到入侵,攻击者就一定会获得未经授权的访问权限,因为他们能够截获用户的合法会话(成功登录后的登录和通信过程),并利用这些会话冒充用户。
- 使用强大的安全密码存储机制,可以降低用户密码以明文泄露的风险。不过,获得认证服务器访问权限的攻击者仍可能植入密码截获后门),并在登录过程中窃取每位用户的明文凭据(用户名 + 密码)。
- 可以按如下方式阻止带后门的服务器攻击:客户端生成随机数 r,并发送 HMAC(password, r) 作为认证信息;服务器将该 HMAC 与存储的密码进行比较。此过程可以结合客户端 Scrypt 或 Argon2 计算,以及服务器端安全存储的密码(经 Scrypt 或 Argon2 哈希)。在这种场景下,只要客户端软件未被攻破,获得认证服务器访问权限的攻击者就无法得到用户明文密码。
- 在 Web 应用中,如果服务器遭到入侵,它可以注入 JavaScript 代码来攻破客户端本身。对于桌面和移动应用,服务器遭到入侵时,客户端相对更加安全。
关于如何为网站、应用和服务实现安全的基于密码认证,可以得出以下结论:
- 使用安全密码存储,例如采用足够强配置的 Argon2 哈希。
- 尽可能保证认证服务器安全。认证服务器一旦遭到入侵,整个系统都会受到影响。
- 在认证服务器与客户端之间使用 TLS 或其他安全通信通道。