Go 中使用 KDF 进行密码哈希和验证
2019 年 12 月 12 日存储密码认证信息是需要非常小心的,否则会有泄漏的风险。
KDF (key derivation function,密钥派生函数) 可以通过特定算法将密码派生出指定长度的密钥,非常适用于密码验证的密码哈希处理。
常用于密码哈希的 KDF 算法有 bcrypt,scrypt,Argon2,PBKDF2。在 2013 到 2015 年间进行的密码哈希竞赛宣布 Argon2 成为获胜者,所以目前最为推荐用于密码哈希的算法就是 Argon2。
Go 语言实现
Go 中提供了 golang.org/x/crypto,其中包括常用的 KDF:
- Argon2, golang.org/x/crypto/argon2。
- Bcrypt, golang.org/x/crypto/bcrypt。
- Scrypt, golang.org/x/crypto/scrypt。
- PBKDF2, golang.org/x/crypto/pbkdf2。
- HKDF, golang.org/x/crypto/hkdf。
KDFCrypt
直接使用 golang.org/x/crypto
还不算特别方便,用于密码验证还需要额外做些处理,比如生成随机盐值
(salt),保存盐值和参数。
于是我写了 KDFCrypt 这个包用于简化密码哈希和验证的过程。
KDFCrypt 主要提供了不同 KDF 用于密码哈希和验证的 Go 通用接口。这是一个简单的例子:
package main
import (
"fmt"
"github.com/xianghuzhao/kdfcrypt"
)
func main() {
encoded, _ := kdfcrypt.Encode("password", &kdfcrypt.Option{
Algorithm: "argon2id",
Param: "m=65536,t=1,p=4",
RandomSaltLength: 16,
HashLength: 32,
})
// $argon2id$v=19,m=65536,t=1,p=4$mD+rvcR+6nuAV6MJFOmDjw$IqfwTPk9RMGeOv4pCE1QiURuSoi655GUVjcQAk81eXM
fmt.Println(encoded)
match, _ := kdfcrypt.Verify("password", encoded)
fmt.Println(match) // true
}
KDFCrypt 包含两个重要的函数 Encode
和 Verify
。Encode
根据指定参数生成一个编码后的字符串,字符串的形式为:
$argon2id$v=19,m=4096,t=1,p=1$4ns1ibGJDR6IQufkbT8E/w$WQ2lAwbDhZmZQMCMg74L00OHUFzn/IvbwDaxU6bgIys
$ KDF $ param $ salt (base64) $ hash (base64)
这个字符串包含了用于密码验证的所有信息,可以直接安全保存下来,以供
Verify
函数验证。
kdfcrypt.Option
kdfcrypt.Option
struct 作为参数传递给 Encode
函数。包括以下选项:
- Algorithm:目前可选的算法有
argon2id
,argon2i
,scrypt
,pbkdf
,hkdf
。 - Param:算法参数字符串。每种算法需要的参数类型不同,多个参数可以用逗号分隔开。更详细的参数参见 README。
- RandomSaltLength:随机盐值的字节数长度。选择合适长度的随机盐值,可以有效防止彩虹表破解。
- Salt:指定盐值。如果选择此项,
RandomSaltLength
将被忽略。 - HashLength:最终密码哈希生成的字节数。
虽然有多种算法可选,但是目前最推荐的还是
argon2id
。这里没有包含bcrypt
算法,是由于golang.org/x/crypto/bcrypt
并没有提供专门生成不同长度哈希的函数,无法直接用作 KDF,我曾经尝试把它加入进去,但是导致接口和数据变得复杂且不一致,所以干脆将它舍去。
直接使用 KDF
某些情况下需要直接使用 KDF 生成的密钥,也可以通过
kdfcrypt
实现。详细的接口函数参见
GoDoc.
生成 AES-256 密钥
AES-256
算法需要使用 32 字节的密钥,可以这样生成:
kdf, err := kdfcrypt.CreateKDF("argon2id", "m=4096,t=1,p=1")
salt, err := kdfcrypt.GenerateRandomSalt(16)
aes256Key, err := kdf.Derive("password", salt, 32)