之前常用Python Flask框架写Web应用,涉及到用户密码这一点,一般是用werkzeug的generate_password_hash函数。

现在写一些新应用开始用Go了,但有时候两者间可能要进行用户密码统一认证,要求双方都能验证 对方生成的密码。

参照了网上的几个案例,虽然自身可以生成、验证,但换到对方反而不行,所以参考 pbkdf2 算法和 werkzeug 的实现函数,重新写了下。

需要注意的是,限定使用了 sha256 哈希算法。

package util

import (
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"math/rand"
	"strconv"
	"strings"

	"golang.org/x/crypto/pbkdf2"
)

const SALT_LENGTH = 8
const SALT_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
const DEFAULT_PBKDF2_ITERATIONS = 150000
const DEFAULT_PBKDF2_METHOD = "pbkdf2:sha256"

func genSalt() string {
	var bytes = make([]byte, SALT_LENGTH)
	rand.Read(bytes)
	for k, v := range bytes {
		bytes[k] = SALT_CHARS[v%byte(len(SALT_CHARS))]
	}
	return string(bytes)
}

func rendeMethod(iter int) string {
	return fmt.Sprintf("%s:%d", DEFAULT_PBKDF2_METHOD, iter)
}

func hashInternal(password, salt string, iter int) string {
	t := pbkdf2.Key([]byte(password), []byte(salt), iter, 32, sha256.New)
	pwh := hex.EncodeToString(t)
	return fmt.Sprintf("%s$%s$%s", rendeMethod(iter), salt, pwh)
}

func GeneratePasswordHash(password string) string {
	salt := genSalt()
	return hashInternal(password, salt, DEFAULT_PBKDF2_ITERATIONS)
}

func CheckPasswordHash(pwhash string, password string) bool {
	if strings.Count(pwhash, "$") < 2 {
		return false
	}
	pws := strings.Split(pwhash, "$")
	method := pws[0]
	salt := pws[1]
	if !strings.HasPrefix(method, DEFAULT_PBKDF2_METHOD) {
		return false
	}
	if strings.Count(method, ":") < 2 {
		return false
	}
	mds := strings.Split(method, ":")
	iter, err := strconv.Atoi(mds[2])
	if err != nil {
		return false
	}

	return strings.EqualFold(pwhash, hashInternal(password, salt, iter))
}

测试用例(go):

package util

import (
	"strings"
	"testing"
)

func TestPasswd(t *testing.T) {
	pwd := "Abc123@$-*&"
    // pwhash 是下面python测试用例中的密码
	pwhash := GeneratePasswordHash(pwd)

	if !strings.HasPrefix(pwhash, "pbkdf2") {
		t.Fatal("hash password format error")
	}
	if !CheckPasswordHash(pwhash, pwd) {
		t.Fatal("CheckPasswordHash error (true)")
	}

	if CheckPasswordHash(pwhash, "ejaA5W4866") {
		t.Fatal("CheckPasswordHash error (false)")
	}

	// 验证 werkzeug generate_password_hash 生成的密码
	pypwhash := "pbkdf2:sha256:150000$XqnEnB37$cf2bffd0ccf0ae5d5c7c9486a6c3dc8f4add525f6faf9c4d85c91e98bb360b86"
	if !CheckPasswordHash(pypwhash, "123456") {
		t.Fatal("CheckPasswordHash error (werkzeug)")
	}
}

测试用例(python):

from werkzeug.security import check_password_hash
check_password_hash("pbkdf2:sha256:150000$ufehHgNr$48915f0de52612c5df20946d30c2b7e2619896348ac7f5aaf07d59cbea1cfb18","Abc123@$-*&")

·End·