完善不同加密方法的使用
This commit is contained in:
+136
-11
@@ -1,19 +1,144 @@
|
||||
// Package bcryptx 提供基于 bcrypt 的口令哈希。
|
||||
//
|
||||
// bcrypt 是经过长期考验的口令哈希函数,内置随机盐并把代价参数编码在输出串中,
|
||||
// 因此适合直接存储。它的缺点是内存开销固定且较小,抗 GPU/ASIC 能力不如
|
||||
// Argon2id;新项目在无兼容性约束时建议优先考虑 argon2x。
|
||||
//
|
||||
// # 使用要点
|
||||
//
|
||||
// - 输出串已经包含算法标识、代价与盐,直接入库即可,不要自行加盐或再做哈希;
|
||||
// - bcrypt 只使用口令的**前 72 字节**,本包会对超长口令直接报错而不是静默截断,
|
||||
// 以免出现「不同口令却能互相通过校验」的意外;
|
||||
// - 校验必须走本包的 VerifyPassword,内部使用恒定时间比较,绝不要用 == 比较
|
||||
// 哈希串。
|
||||
package bcryptx
|
||||
|
||||
import "golang.org/x/crypto/bcrypt"
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
// HashPassword 使用 bcrypt 算法对密码进行哈希处理
|
||||
func HashPassword(password string) (string, error) {
|
||||
// 第二个参数是 cost 值,代表计算强度
|
||||
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return "", err
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
const (
|
||||
// MinCost 是最小代价,仅适合测试:代价每 +1,计算量翻倍。
|
||||
MinCost = bcrypt.MinCost
|
||||
// DefaultCost 是默认代价(10),在安全性与延迟之间较均衡。
|
||||
DefaultCost = bcrypt.DefaultCost
|
||||
// MaxCost 是最大代价(31)。在普通硬件上几乎无法完成,慎用。
|
||||
MaxCost = bcrypt.MaxCost
|
||||
// MaxPasswordLength 是 bcrypt 实际参与运算的口令长度上限(字节)。
|
||||
//
|
||||
// 超出部分会被算法忽略,因此本包对超长口令返回错误,而不是静默截断:
|
||||
// 静默截断会让两个不同的长口令互相通过校验。
|
||||
MaxPasswordLength = 72
|
||||
)
|
||||
|
||||
// ErrInvalidHash 表示哈希串格式非法(不是合法的 bcrypt 输出)。
|
||||
var ErrInvalidHash = errors.New("bcryptx: invalid bcrypt hash")
|
||||
|
||||
// ErrPasswordTooLong 表示口令超过 bcrypt 能处理的长度上限(MaxPasswordLength)。
|
||||
//
|
||||
// 哈希与校验两条路径都会返回它,绝不静默截断:bcrypt 只使用前 72 字节,静默
|
||||
// 截断会让两个只有后缀不同的长口令互相通过校验。
|
||||
var ErrPasswordTooLong = errors.New("bcryptx: password is too long for bcrypt")
|
||||
|
||||
// ValidCost 报告 cost 是否落在 bcrypt 支持的范围内。
|
||||
func ValidCost(cost int) bool {
|
||||
return cost >= MinCost && cost <= MaxCost
|
||||
}
|
||||
|
||||
// HashPassword 使用默认代价(DefaultCost)对口令做哈希,返回可直接入库的字符串。
|
||||
func HashPassword(password []byte) (string, error) {
|
||||
return HashPasswordWithCost(password, DefaultCost)
|
||||
}
|
||||
|
||||
// HashPasswordWithCost 使用指定代价对口令做哈希。
|
||||
//
|
||||
// 代价越高越难被暴力破解,但验证时的耗时也同步增加。cost 必须落在
|
||||
// [MinCost, MaxCost] 内。每次调用都会使用新的随机盐,因此同一口令两次调用的
|
||||
// 结果不同,这是预期行为。
|
||||
func HashPasswordWithCost(password []byte, cost int) (string, error) {
|
||||
if !ValidCost(cost) {
|
||||
return "", fmt.Errorf("bcryptx: 代价 %d 超出 [%d,%d]", cost, MinCost, MaxCost)
|
||||
}
|
||||
return string(bytes), nil
|
||||
if len(password) == 0 {
|
||||
return "", errors.New("bcryptx: 口令不能为空")
|
||||
}
|
||||
if len(password) > MaxPasswordLength {
|
||||
return "", fmt.Errorf("%w: 长度 %d 超过上限 %d 字节;"+
|
||||
"超出部分会被 bcrypt 忽略,静默截断会让不同长口令互相通过校验;"+
|
||||
"请改用 argon2x 或先对长口令做一次摘要",
|
||||
ErrPasswordTooLong, len(password), MaxPasswordLength)
|
||||
}
|
||||
|
||||
hashed, err := bcrypt.GenerateFromPassword(password, cost)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("bcryptx: 生成本口令哈希失败: %w", err)
|
||||
}
|
||||
return string(hashed), nil
|
||||
}
|
||||
|
||||
// CheckPasswordHash 检查密码是否与哈希值匹配
|
||||
// VerifyPassword 校验口令是否与哈希匹配。
|
||||
//
|
||||
// 返回值语义经过刻意区分:
|
||||
// - (true, nil):匹配;
|
||||
// - (false, nil):不匹配(口令错误)——这是正常的业务分支;
|
||||
// - (false, err):哈希串非法、或口令超过 bcrypt 的长度上限——属于调用方
|
||||
// 需要处理的问题,应当记录告警而不是当作"密码错误"。
|
||||
//
|
||||
// 与 HashPassword 一致,本函数**不会截断**超长口令:若允许截断,任何超过 72 字节
|
||||
// 且在 72 字节处共享前缀的口令都能通过校验。
|
||||
//
|
||||
// 内部使用 bcrypt 自带的恒定时间比较,不会泄漏时序信息。
|
||||
func VerifyPassword(password []byte, hash string) (bool, error) {
|
||||
if hash == "" {
|
||||
return false, ErrInvalidHash
|
||||
}
|
||||
if len(password) > MaxPasswordLength {
|
||||
return false, fmt.Errorf("%w: 长度 %d 超过上限 %d 字节", ErrPasswordTooLong, len(password), MaxPasswordLength)
|
||||
}
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hash), password)
|
||||
switch {
|
||||
case err == nil:
|
||||
return true, nil
|
||||
case errors.Is(err, bcrypt.ErrMismatchedHashAndPassword):
|
||||
return false, nil
|
||||
case errors.Is(err, bcrypt.ErrHashTooShort):
|
||||
return false, fmt.Errorf("%w: %w", ErrInvalidHash, err)
|
||||
default:
|
||||
return false, fmt.Errorf("%w: %w", ErrInvalidHash, err)
|
||||
}
|
||||
}
|
||||
|
||||
// CheckPasswordHash 是 VerifyPassword 的便捷布尔包装。
|
||||
//
|
||||
// 它把「哈希串非法」与「口令不匹配」都归为 false,适合不关心失败原因的场合。
|
||||
// 需要区分两者时请使用 VerifyPassword。
|
||||
func CheckPasswordHash(password, hash string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||
return err == nil
|
||||
ok, err := VerifyPassword([]byte(password), hash)
|
||||
return err == nil && ok
|
||||
}
|
||||
|
||||
// Cost 从哈希串中解析出代价参数。
|
||||
//
|
||||
// 用于在登录成功后判断旧哈希是否需要按新的代价标准重算。
|
||||
func Cost(hash string) (int, error) {
|
||||
cost, err := bcrypt.Cost([]byte(hash))
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("%w: %w", ErrInvalidHash, err)
|
||||
}
|
||||
return cost, nil
|
||||
}
|
||||
|
||||
// NeedsRehash 报告给定哈希的代价是否低于 targetCost。
|
||||
//
|
||||
// 典型用法:登录校验通过后调用本函数,若为 true 则用新代价重新哈希并更新存储,
|
||||
// 从而在不强制用户改密的前提下逐步提升整体强度。
|
||||
func NeedsRehash(hash string, targetCost int) bool {
|
||||
cost, err := Cost(hash)
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
return cost < targetCost
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user