因为项目需要从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
}