背景
对于每一个提供用户账密认证方式的应用系统来说,密码存储都是一个非常至关重要的设计内容。系统实现过程的一个重要设计原则是,需要在攻击者对整个系统设计都了如指掌的前提下,系统的安全性仍然能够得到保证,即内部研发人员要无法作恶。而单单针对密码存储来讲,我们需要确保的是对应密码存储手段能够在攻击者拿到密码存储的内容时也无法逆向出来用户的密码。
OWASP组织针对密码存储提供了一个专题指引,本文针对里面的密码存储方法进行了翻译,并对密码存储(or 密码哈希)算法基于Go语言的crypto库实现做了一下的性能测试,并收集了对应算法的实现标准链接,方便后续进一步学习。
密码存储 “哈希” vs “加密”
哈希和加密都能安全保存敏感数据信息,但是在绝大部分的场景中,密码存储都是哈希,而不是加密。
其原因在于,哈希是一个“单向(One-way)”函数,几乎不能基于一个哈希后的取值逆向出来原文。即使攻击者拿到了密码哈希后的内容,攻击者也不能使用该内容用来恶意登录。
而加密是一个“双向(two-way)”函数,攻击者在理论上是可以通过加密内容获取到原文的。加密更适合存储用户的地址信息等个人敏感数据,这种信息的特点是需要展示原文在用户界面上。
针对密码存储增强的方法
Salting - 加盐
salt - “盐”,是一个唯一的,作为哈希过程的一部分添加到每个密码中。由于每个用户密码的盐是唯一的,攻击者必须使用各自的盐一次破解一个哈希,而不是一次计算一个哈希值并将其与每个密码存储的哈希进行比较。这使得破解大量哈希变得更加困难,因为所需的时间与哈希数量成正比。
加盐还可以防止攻击者使用彩虹表或基于数据库的查找来预先计算散列。最后,盐化意味着如果不破解哈希,就不可能确定两个用户是否拥有相同的密码,因为即使密码相同,不同的盐也会导致不同的哈希。
现代散列算法(如Argon2id、bcrypt和PBKDF2)会自动对密码进行盐化处理,因此在使用它们时不需要额外的步骤。
Peppering - 胡椒
相对于做菜来讲,每道菜需要使用的盐是不一样的,而“胡椒”是在每一道菜中作为一个调料品,可以固定添加。---译者著
除了加盐之外,还可以用胡椒来提供额外的保护层。如果攻击者只有对数据库的访问权限,比如说他们利用了SQL注入漏洞或获得了数据库的备份,那么它可以防止攻击者破解任何散列。胡椒策略不会以任何方式影响密码散列函数。
举例来讲,一种“胡椒”策略是像往常一样对密码进行哈希(使用密码哈希算法),然后在将密码哈希存储到数据库中之前,在原始密码哈希上使用HMAC(例如,HMAC-SHA256, HMAC-SHA512,取决于所需的输出长度),胡椒充当HMAC密钥。
- 胡椒在存储的密码之间是共享的,而不是像盐一样唯一。
- 与密码盐不同,“胡椒”不应该存储在数据库中。
- “胡椒”是一个秘密,应该存放在“Secret Valuts”或HSM(硬件安全模块)中。
- 像任何其他加密密钥一样,应该考虑对其做一个轮转策略控制。
Using Work Facators - 使用负载因子
负载因子是为每个密码执行哈希算法的迭代次数(通常,实际上是2^工作迭代)。负载因子通常存储在哈希输出中。它使得计算哈希的计算成本更高,这反过来降低了攻击者试图破解密码哈希的速度或增加了破解成本。
在选择负载因子时,要在安全性和性能之间取得平衡。虽然更高的负载系数使攻击者更难破解哈希,但它们会减慢验证登录尝试的过程。如果负载因子过高,则可能会降低应用程序的性能,攻击者可能会利用这一点通过使用大量登录尝试耗尽服务器的CPU来执行拒绝服务攻击。
理想的负载系数没有黄金法则——它取决于服务器的性能和应用程序上的用户数量。确定最佳负载因子需要在应用程序使用的特定服务器上进行实验。作为一般规则,计算哈希值应该少于1秒。
具有负载系数的一个关键优势是,随着硬件变得更强大和更便宜,它可以随着时间的推移而增加。
升级负载系数的最常见方法是等待用户下一次身份验证,然后用新的负载系数重新散列他们的密码。不同的散列将具有不同的负载因子,如果用户不重新登录到应用程序,则可能永远不会升级散列值。根据应用程序的不同,删除旧的密码散列并要求用户在下次登录时重置密码可能是合适的,以避免存储旧的和不太安全的密码哈希值。
算法 - Go语言实现测试
下列所有的测试数据都基于Mac mini M2芯片个人电脑计算得出。
测试参数为:-test.benchtime 10s
Argon2id
Password Hashing Competition:https://www.password-hashing.net/
RFC文档:https://datatracker.ietf.org/doc/html/rfc9106
其中OWASP推荐的几个参数配置如下,这些参数配置组合能够提供同等强度的密码哈希,其中的差别在于CPU和RAM的均衡使用:
M,最小内存,单位为KiB |
t,最小迭代次数 |
p,并行度 |
|
1 |
46 * 1024(46M) |
1 |
1 |
2 |
19 * 1024(19M) |
2 |
1 |
3 |
12 * 1024(12M) |
3 |
1 |
4 |
9 * 1024(9M) |
4 |
1 |
5 |
7 * 1024(7M) |
5 |
1 |
func Argon2id_46_1_1(password []byte) []byte { passwordHash := argon2.IDKey(password, random_salt(), 1, 46*1024, 1, 96) return passwordHash } func Argon2id_19_2_1(password []byte) []byte { passwordHash := argon2.IDKey(password, random_salt(), 2, 19*1024, 1, 96) return passwordHash } func Argon2id_12_3_1(password []byte) []byte { passwordHash := argon2.IDKey(password, random_salt(), 3, 12*1024, 1, 96) return passwordHash } func Argon2id_9_4_1(password []byte) []byte { passwordHash := argon2.IDKey(password, random_salt(), 4, 9*1024, 1, 96) return passwordHash } func Argon2id_7_5_1(password []byte) []byte { passwordHash := argon2.IDKey(password, random_salt(), 5, 7*1024, 1, 96) return passwordHash }
测试结果:
OWASP优先推荐Argon2id作为密码哈希算法,并建议其最小的参数设置为
scrypt
RFC标准文档:https://datatracker.ietf.org/doc/html/rfc7914#page-12
https://www.cnblogs.com/flydean/p/15405264.html
其中OWASP推荐的几个参数配置如下,这些参数配置组合能够提供同等强度的密码哈希,其中的差别在于CPU和RAM的均衡使用:
N,CPU/内存耗费比 |
r,块大小 |
p,并行度 |
|
1 |
2^17 (128MiB) |
8 |
1 |
2 |
2^16 (128MiB) |
8 |
2 |
3 |
2^15 (128MiB) |
8 |
3 |
4 |
2^14 (128MiB) |
8 |
5 |
5 |
2^13 (128MiB) |
8 |
10 |
func Scrypt_N2_13_R8_P10(password []byte) []byte { passwordHash, err := scrypt.Key(password, random_salt(), 1<<13, 8, 10, 96) if err != nil { panic(err) } return passwordHash } func Scrypt_N2_14_R8_P5(password []byte) []byte { passwordHash, err := scrypt.Key(password, random_salt(), 1<<14, 8, 5, 96) if err != nil { panic(err) } return passwordHash } func Scrypt_N2_15_R8_P3(password []byte) []byte { passwordHash, err := scrypt.Key(password, random_salt(), 1<<15, 8, 3, 96) if err != nil { panic(err) } return passwordHash } func Scrypt_N2_16_R8_P2(password []byte) []byte { passwordHash, err := scrypt.Key(password, random_salt(), 1<<16, 8, 2, 96) if err != nil { panic(err) } return passwordHash } func Scrypt_N2_17_R8_P1(password []byte) []byte { passwordHash, err := scrypt.Key(password, random_salt(), 1<<17, 8, 1, 96) if err != nil { panic(err) } return passwordHash }
测试结果:
bcrypt
Paper: https://www.usenix.org/legacy/event/usenix99/provos/provos.pdf
https://php.watch/versions/8.4/password_hash-bcrypt-cost-increase
bcrypt的控制参数只有Cost,不同Cost之间的迭代次数会存在指数差别:
func Bcrypt_Cost10(password []byte) []byte { if len(password) > 72 { h := sha256.New() password = h.Sum(password) } passwordHash, err := bcrypt.GenerateFromPassword(password, bcrypt.DefaultCost) if err != nil { panic(err) } return passwordHash } func Bcrypt_Cost12(password []byte) []byte { if len(password) > 72 { h := sha256.New() password = h.Sum(password) } passwordHash, err := bcrypt.GenerateFromPassword(password, 12) if err != nil { panic(err) } return passwordHash } func Bcrypt_Cost14(password []byte) []byte { if len(password) > 72 { h := sha256.New() password = h.Sum(password) } passwordHash, err := bcrypt.GenerateFromPassword(password, 14) if err != nil { panic(err) } return passwordHash }
测试结果:
pbkdf2
PBDKF介绍:https://www.cnblogs.com/flydean/p/15346657.html
RFC:https://datatracker.ietf.org/doc/html/rfc8018
其中OWASP推荐的几个参数配置如下:
算法 |
迭代次数 |
PBKDF2-HMAC-SHA1 |
1300000次迭代 |
PBKDF2-HMAC-SHA256 |
600000次迭代 |
PBKDF2-HMAC-SHA512 |
210000次迭代 |
func Pbkdf2_sha1_1300000(password []byte) []byte { return pbkdf2.Key(password, random_salt(), 1_300_000, 96, sha1.New) } func Pbkdf2_sha256_600000(password []byte) []byte { return pbkdf2.Key(password, random_salt(), 600_000, 96, sha256.New) } func Pbkdf2_sha512_210000(password []byte) []byte { return pbkdf2.Key(password, random_salt(), 210_000, 96, sha512.New) }
测试结果:
总结
OWASP针对上述4种算法的推荐顺序如下:Argon2id > scrypt > bcrypt。如果需要满足FIPS-140(Federal Information Processing Standards)合规的情况下,使用PBKDF2-HMAC-SHA256,负载因子为600000次迭代是一个不错的选择。
在实际应用中,如果以PBKDF2-SHA256,迭代次数60W次作为baseline来看的话,我们单次验证密码的时间需要耗费0.2s左右,这种计算强度即能够一定程度上抵抗暴力攻击,又不会存在较大的被DOS攻击风险。同时结合“胡椒”方法,我们能够获得较强的密码攻击防御能力。
参考文档
- OWASP:https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html
- Password Cryptography Specification: https://datatracker.ietf.org/doc/html/rfc8018
- scrypt - RFC7914:https://datatracker.ietf.org/doc/html/rfc7914#page-12
- RFC8018:https://datatracker.ietf.org/doc/html/rfc8018
- HMAC算法及计算流程介绍
- [译] 密码哈希的方法:PBKDF2,Scrypt,Bcrypt 和 ARGON2
- GO语言 crypto库针对password hash的支持:https://pkg.go.dev/golang.org/x/crypto
- Bcrypt Paper: https://www.usenix.org/legacy/event/usenix99/provos/provos.pdf
- Password Hashing Competition:https://www.password-hashing.net/
- RFC 9106:https://datatracker.ietf.org/doc/html/rfc9106
- https://www.cnblogs.com/flydean/p/15405264.html
- https://php.watch/versions/8.4/password_hash-bcrypt-cost-increase