- 将现有练习及 module 文件迁移至 go/practice,保持代码和依赖不变 - 新增 go/projects 项目区说明,更新目录约定与开发命令 BREAKING CHANGE: Go 练习命令需改在 go/practice 目录下执行
150 lines
2.9 KiB
Go
150 lines
2.9 KiB
Go
package crypto
|
|
|
|
import (
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
"golang.org/x/crypto/bcrypt"
|
|
)
|
|
|
|
var (
|
|
ErrInvalidPassword = errors.New("invalid password")
|
|
ErrInvalidHash = errors.New("invalid password hash")
|
|
)
|
|
|
|
type Hasher struct {
|
|
cost int // cost 可以理解成 bcrypt 的“计算难度”参数,cost 越高,计算 bcrypt hash 就越慢,攻击者暴力破解密码也越贵
|
|
}
|
|
|
|
func NewHasher(cost int) (*Hasher, error) {
|
|
if cost < bcrypt.MinCost || cost > bcrypt.MaxCost {
|
|
return nil, errors.New("invalid bcrypt cost")
|
|
}
|
|
|
|
return &Hasher{
|
|
cost: cost,
|
|
}, nil
|
|
}
|
|
|
|
// Hash 用于注册、重置密码、修改密码。
|
|
func (h *Hasher) Hash(password string) (string, error) {
|
|
if len(password) == 0 {
|
|
return "", ErrInvalidPassword
|
|
}
|
|
|
|
// bcrypt 只使用密码前 72 bytes。
|
|
// 生产环境建议显式拒绝,而不是静默截断。
|
|
if len([]byte(password)) > 72 {
|
|
return "", ErrInvalidPassword
|
|
}
|
|
|
|
hash, err := bcrypt.GenerateFromPassword(
|
|
[]byte(password),
|
|
h.cost,
|
|
)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return string(hash), nil
|
|
}
|
|
|
|
// Verify 用于登录时验证密码。
|
|
func (h *Hasher) Verify(password, encodedHash string) (bool, error) {
|
|
if len(password) == 0 || len([]byte(password)) > 72 {
|
|
return false, ErrInvalidPassword
|
|
}
|
|
|
|
if encodedHash == "" {
|
|
return false, ErrInvalidHash
|
|
}
|
|
|
|
err := bcrypt.CompareHashAndPassword(
|
|
[]byte(encodedHash),
|
|
[]byte(password),
|
|
)
|
|
|
|
switch {
|
|
case err == nil:
|
|
return true, nil
|
|
|
|
case errors.Is(err, bcrypt.ErrMismatchedHashAndPassword):
|
|
return false, nil
|
|
|
|
default:
|
|
// 例如数据库里的 hash 已损坏或格式不合法。
|
|
return false, ErrInvalidHash
|
|
}
|
|
}
|
|
|
|
// NeedsRehash 判断数据库中的 hash 是否使用了较旧的 cost。
|
|
func (h *Hasher) NeedsRehash(encodedHash string) bool {
|
|
cost, err := bcrypt.Cost([]byte(encodedHash))
|
|
if err != nil {
|
|
return true
|
|
}
|
|
|
|
return cost != h.cost
|
|
}
|
|
|
|
func TestBcrypt(t *testing.T) {
|
|
oldHasher, err := NewHasher(10)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
newHasher, err := NewHasher(12)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
password := "password"
|
|
oldHash, err := oldHasher.Hash(password)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Log(oldHash)
|
|
newHash, err := newHasher.Hash(password)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Log(newHash)
|
|
err = bcrypt.CompareHashAndPassword([]byte(oldHash), []byte(password))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
}
|
|
|
|
func TestHashPassword(t *testing.T) {
|
|
hasher, err := NewHasher(12)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
password := "password"
|
|
hash, err := hasher.Hash(password)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Log(hash)
|
|
if ok, err := hasher.Verify(password, hash); err != nil || !ok {
|
|
t.Fatal("verify failed")
|
|
}
|
|
if ok := hasher.NeedsRehash(hash); !ok {
|
|
t.Fatal("needs rehash failed")
|
|
}
|
|
|
|
}
|
|
|
|
func TestGenerateTime(t *testing.T) {
|
|
start := time.Now()
|
|
|
|
_, err := bcrypt.GenerateFromPassword(
|
|
[]byte("TestPassword123!"),
|
|
14,
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Log(time.Since(start), err)
|
|
}
|