密码哈希
约 1176 个字 预计阅读时间 4 分钟
介绍
密码哈希:如何安全地存储密码?
密码:用户输入的用于证明其身份的秘密字符串
- 当创建服务帐户时:创建密码
- 当稍后想要登录该服务时:再次输入相同的密码
该服务如何检查密码是否正确?
- Bad Idea1:存储列出每个用户密码的文件
- Q: 如果攻击者侵入服务怎么办? 现在攻击者知道每个人的密码!
- Bad Idea2:在存储每个用户的密码之前对其进行加密
- Q:攻击者可以窃取密码文件和密钥并解密每个人的密码!
需要一种方法来验证密码,而不存储任何允许某人恢复原始密码的信息
- 对于每个用户,存储其密码的哈希值
- 验证流程:
- 对用户提交的密码进行哈希处理
- 检查它是否与文件中的密码哈希匹配
- 用到了哈希中的哪些属性?
- 确定性:要验证密码,每次都必须哈希为相同的值
- 单向:我们不希望攻击者将哈希值反转为原始密码
攻击
Question
如果两个不同的用户决定使用password123
作为密码怎么办?
哈希值是确定性的:它们具有相同的密码哈希值;故攻击者可以看到哪些用户使用相同的密码
暴力攻击:
- 大多数人使用不安全的通用密码
- 攻击者可以预先计算常见密码的哈希值:如
H("password123"),H("password1234"),H("1234567890")
等等... - 字典攻击:对整个常用密码字典进行哈希处理
彩虹表(Rainbow tables):一种计算哈希值的算法,使暴力攻击变得更容易
解决方案1:为每个用户添加唯一的随机salt → Salted Hashes
- Salt:一个随机的公共值,旨在使暴力攻击变得更加困难
- 对于每个用户, 存储:
username, salt, H(password || salt)
- 要验证用户:在密码文件中查找其salt,计算
H(password || salt)
,并检查它是否与文件中的哈希值匹配 - salt应该比较长且随机
- salt不是秘密(可以将它想象成随机数或 IVs)
- 对于每个用户, 存储:
- 暴力攻击现在更加困难
- 假设数据库中有 M 个可能的密码和 N 个用户
- Unsalted数据库:对所有可能的密码进行哈希处理,然后查找所有用户的哈希值 → \(O(M + N)\)
- salted数据库:对每个用户的盐的所有密码进行哈希 → \(O(MN)\)
解决方案2: 使用较慢的哈希值
- 加密哈希通常设计得很快
- SHA 旨在尽快生成 1GB 文档的校验和
- 密码哈希通常设计得很慢
- 合法用户只需提交几次密码尝试。用户不会注意到服务器检查密码需要0.0001s还是0.1s
- 攻击者需要计算数百万个哈希值,用慢速哈希可以使攻击者的速度降低1000倍或更多!
Note:并不会改变攻击的渐近难度,添加了一个大的常数因子,就可以对攻击者产生巨大的实际影响
Example
Password-based key derivation function 2 (PBKDF2): 慢速哈希函数
- 设置:输出随机位的底层函数(e.g HMAC-SHA256)
- 设置:所需的输出长度(n)
- 设置:迭代计数(较高 = 散列较慢,较低 = 散列较快)
- 输入:密码 和 salt
- 输出:从密码和salt生成比较长的,随机的n位字符串
- 实现:基本上计算HMAC 10,000次
好处(假设用户密码强度高)
- 从用户密码中产生出任意长的字符串
- 输出可以直接作为对称密钥, 还可用于播种 PRNG 或生成公钥/私钥对
- 算法很慢,但不使用大量内存(Scrypt 和 Argon2 等替代方案使用更多内存)
离线攻击(Offline attack):攻击者自己执行所有计算
- e.g:Mallory 窃取密码文件,然后自己计算哈希值以检查匹配项。
- 攻击者可以尝试大量密码(例如并行使用许多 GPU)
- 防御措施:salt密码、使用慢速哈希
- 如果攻击者可以进行离线攻击,则需要一个非常强的密码(如7个或更多随机单词)
在线攻击(Online attack):攻击者与服务交互
- e.g. Mallory尝试通过尝试不同的密码来登录网站, Mallory正在强制服务器计算哈希值
- 攻击者通常每秒只能尝试几次,没有并行性
- 防御:添加超时或速率限制尝试次数,以防止攻击者尝试过多次数