因为项目需要从Python Django
框架重构为Golang
项目,为了保证用户数据不丢失,所以密码算法使用与Django框架相同的pbkdf2_sha256
加密算法。以下代码根据GitHub
开源项目github.com/alexandrevicenzi/unchained/
修改而成,有兴趣的可以翻阅项目代码。
0. 说明
系统环境
go version go1.14.3 darwin/amd64
1. 简单的加密算法分析
直接拿出一个经过pbkdf2_sha256
加密后的密钥
pbkdf2_sha256$180000$9v62SVRUKPTf$igFByaMbYXhOh7p375LDdo2GYrUV9RbqdTaR6jbhrKg=
密钥一共分为4个部分,分别是加密算法名称
、加密迭代次数
、盐(salt)
、base64编码
,每一个部分都由美元$
符号分割,由此我们解析上面密钥的数据。
- 加密算法名称:pbkdf2_sha256
- 加密迭代次数:180000
- 盐:9v62SVRUKPTf
- Base64编码:igFByaMbYXhOh7p375LDdo2GYrUV9RbqdTaR6jbhrKg=
由于pbkdf2_sha256
属于单向
加密算法(即无法通过密钥反推原始密码),所以我们要校验密码需要用同样的方式来加密,最终将两个加密密钥对比,如果相同则认为密码正确。
2. 加密
需要使用加密算法库golang.org/x/crypto/pbkdf2
,控制台输入如下命令:
go get golang.org/x/crypto/pbkdf2
加密过程我们需要三个参数,分别是原始密码
、盐(Salt)
、加密迭代次数
,其中盐
我们可以随机生成(注意:盐
不能包含美元$
符号,不然会被误认为是分隔符),加密迭代次数
可以随意设置,一般情况下越大则越难被破译,但开销也越大,Django选用180000
次,这里不深入讨论。
func PasswordEncode(password string, salt string, iterations int) (string, error) {
// 一共三个参数,分别是原始密码、盐、迭代次数
// 如果没有设置盐,则使用12位的随机字符串
if strings.TrimSpace(salt) == "" {
salt = CreateRandomString(12)
}
// 确保盐不包含美元$符号
if strings.Contains(salt, "$") {
return "", errors.New("salt contains dollar sign ($)")
}
// 如果迭代次数小于等于0,则设置为180000
if iterations <= 0 {
iterations = 180000
}
// pbkdf2加密 <--- 关键
hash := pbkdf2.Key([]byte(password), []byte(salt), iterations, sha256.Size, sha256.New)
// base64编码成为固定长度的字符串
b64Hash := base64.StdEncoding.EncodeToString(hash)
// 最终字符串拼接成pbkdf2_sha256密钥格式
return fmt.Sprintf("%s$%d$%s$%s", "pbkdf2_sha256", iterations, salt, b64Hash), nil
}
// 随机字符串生成函数(不深入讨论)
func CreateRandomString(len int) string {
var container string
var str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
b := bytes.NewBufferString(str)
length := b.Len()
bigInt := big.NewInt(int64(length))
for i := 0; i < len; i++ {
randomInt, _ := rand.Int(rand.Reader, bigInt)
container += string(str[randomInt.Int64()])
}
return container
}
3. 校验
我们只需要通过将原始密码重新使用按照相同的盐
和迭代次数
加密就能获得对应的密钥,将我们得到的密钥和数据库中的密钥对比,如果相同则认为是正确的密码,如果不相同则认为是不正确的密码。
func PasswordVerify(password string, encoded string) (bool, error) {
// 输入两个参数,分别是原始密码、需要校验的密钥(数据库中存储的密码)
// 输出校验结果(布尔值)、错误
// 先根据美元$符号分割密钥为4个子字符串
s := strings.Split(encoded, "$")
// 如果分割结果不是4个子字符串,则认为不是pbkdf2_sha256算法的结果密钥,跳出错误
if len(s) != 4 {
return false, errors.New("hashed password components mismatch")
}
// 分割子字符串的结果分别为算法名、迭代次数、盐和base64编码
// ---> 这里可以获得加密用的盐
algorithm, iterations, salt := s[0], s[1], s[2]
// 如果密钥算法名不是pbkdf2_sha256算法,跳出错误
if algorithm != "pbkdf2_sha256" {
return false, errors.New("algorithm mismatch")
}
// 将迭代次数转换成int数据类型 -->这里可以获得加密用的迭代次数
i, err := strconv.Atoi(iterations)
if err != nil {
return false, errors.New("unreadable component in hashed password")
}
// 将原始密码用上面获取的盐、迭代次数进行加密
newEncoded, err := PasswordEncode(password, salt, i)
if err != nil {
return false, err
}
// 最终用hmac.Equal函数判断两个密钥是否相同
return hmac.Equal([]byte(newEncoded), []byte(encoded)), nil
}