Files
encryptx/bcryptx/bcryptx_test.go
T

196 lines
5.8 KiB
Go
Raw Normal View History

2025-11-09 18:23:08 +08:00
package bcryptx_test
import (
2026-09-30 15:49:12 +08:00
"bytes"
"errors"
"strings"
2025-11-09 18:23:08 +08:00
"testing"
"code.yun.ink/pkg/encryptx/bcryptx"
)
2026-09-30 15:49:12 +08:00
func TestHashAndVerify(t *testing.T) {
password := []byte("correct horse battery staple")
hash, err := bcryptx.HashPassword(password)
2025-11-09 18:23:08 +08:00
if err != nil {
2026-09-30 15:49:12 +08:00
t.Fatalf("HashPassword 失败: %v", err)
2025-11-09 18:23:08 +08:00
}
2026-09-30 15:49:12 +08:00
if !strings.HasPrefix(hash, "$2") {
t.Errorf("哈希串前缀异常: %q", hash)
}
ok, err := bcryptx.VerifyPassword(password, hash)
if err != nil {
t.Fatalf("VerifyPassword 返回错误: %v", err)
}
if !ok {
t.Error("正确口令校验失败")
}
// 错误口令:必须是 (false, nil),而不是 error
ok, err = bcryptx.VerifyPassword([]byte("wrong password"), hash)
if err != nil {
t.Errorf("错误口令不应返回 error, 得到 %v", err)
}
if ok {
t.Error("错误口令通过了校验")
}
if !bcryptx.CheckPasswordHash(string(password), hash) {
t.Error("CheckPasswordHash 对正确口令返回 false")
}
if bcryptx.CheckPasswordHash("wrong", hash) {
t.Error("CheckPasswordHash 对错误口令返回 true")
}
}
// TestSaltIsRandom 验证同一口令两次哈希结果不同(内置随机盐)。
func TestSaltIsRandom(t *testing.T) {
password := []byte("same password")
a, err := bcryptx.HashPassword(password)
if err != nil {
t.Fatalf("HashPassword 失败: %v", err)
}
b, err := bcryptx.HashPassword(password)
if err != nil {
t.Fatalf("HashPassword 失败: %v", err)
}
if a == b {
t.Error("两次哈希结果相同,随机盐可能未生效")
}
// 但两者都必须能校验通过
for i, hash := range []string{a, b} {
if !bcryptx.CheckPasswordHash(string(password), hash) {
t.Errorf("哈希 #%d 校验失败", i)
}
}
}
func TestCostValidation(t *testing.T) {
for _, cost := range []int{bcryptx.DefaultCost, bcryptx.MinCost, bcryptx.MaxCost} {
if !bcryptx.ValidCost(cost) {
t.Errorf("ValidCost(%d) = false, 期望 true", cost)
}
}
for _, cost := range []int{-1, 0, 3, 32, 100} {
if bcryptx.ValidCost(cost) {
t.Errorf("ValidCost(%d) = true, 期望 false", cost)
}
if _, err := bcryptx.HashPasswordWithCost([]byte("pw"), cost); err == nil {
t.Errorf("HashPasswordWithCost(cost=%d) 期望错误, 得到 nil", cost)
}
}
}
// TestExplicitCost 用 MinCost 保证测试快速,同时验证代价被写入哈希串。
func TestExplicitCost(t *testing.T) {
hash, err := bcryptx.HashPasswordWithCost([]byte("pw"), bcryptx.MinCost)
if err != nil {
t.Fatalf("HashPasswordWithCost 失败: %v", err)
}
cost, err := bcryptx.Cost(hash)
if err != nil {
t.Fatalf("Cost 失败: %v", err)
}
if cost != bcryptx.MinCost {
t.Errorf("解析出的代价 = %d, 期望 %d", cost, bcryptx.MinCost)
}
}
// TestLongPasswordRejected 验证超长口令被拒绝而不是被静默截断到 72 字节。
//
// bcrypt 只使用前 72 字节;若静默截断,两个前 72 字节相同的不同口令将互相
// 通过校验,这是一个容易被忽略的认证缺陷。
func TestLongPasswordRejected(t *testing.T) {
tooLong := bytes.Repeat([]byte("a"), bcryptx.MaxPasswordLength+1)
if _, err := bcryptx.HashPassword(tooLong); !errors.Is(err, bcryptx.ErrPasswordTooLong) {
t.Errorf("超长口令期望 ErrPasswordTooLong, 得到 %v", err)
}
// 恰好 72 字节应当可用
exact := bytes.Repeat([]byte("a"), bcryptx.MaxPasswordLength)
hash, err := bcryptx.HashPasswordWithCost(exact, bcryptx.MinCost)
if err != nil {
t.Fatalf("72 字节口令应当被接受, 得到 %v", err)
}
if !bcryptx.CheckPasswordHash(string(exact), hash) {
t.Error("72 字节口令校验失败")
}
// 关键回归:校验路径也必须拒绝超长口令,而不是截断到 72 字节后放行。
//
// bcrypt 只看前 72 字节,若校验时不校验长度,任何"前 72 字节与真实口令相同"
// 的超长口令都会通过——等于凭空扩大了可接受的口令集合。
ok, err := bcryptx.VerifyPassword(tooLong, hash)
if !errors.Is(err, bcryptx.ErrPasswordTooLong) {
t.Errorf("超长口令校验期望 ErrPasswordTooLong, 得到 %v", err)
}
if ok {
t.Error("超长口令被截断后通过了校验(安全缺陷)")
}
if bcryptx.CheckPasswordHash(string(tooLong), hash) {
t.Error("CheckPasswordHash 对超长口令返回 true")
}
}
func TestEmptyPasswordRejected(t *testing.T) {
if _, err := bcryptx.HashPassword(nil); err == nil {
t.Error("空口令期望被拒绝")
}
if _, err := bcryptx.HashPassword([]byte{}); err == nil {
t.Error("空口令期望被拒绝")
}
}
func TestInvalidHashInputs(t *testing.T) {
badHashes := []string{
"",
"not-a-hash",
"$2a$10$",
"$2a$10$tooshort",
"$9x$10$abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstu",
strings.Repeat("x", 100),
}
for _, bad := range badHashes {
// 不得 panic
ok, err := bcryptx.VerifyPassword([]byte("pw"), bad)
if ok {
t.Errorf("非法哈希 %q 校验返回 true", bad)
}
if err == nil {
t.Errorf("非法哈希 %q 期望返回 error, 得到 nil", bad)
}
if !errors.Is(err, bcryptx.ErrInvalidHash) {
t.Errorf("非法哈希 %q: 期望 ErrInvalidHash, 得到 %v", bad, err)
}
if bcryptx.CheckPasswordHash("pw", bad) {
t.Errorf("CheckPasswordHash 对非法哈希 %q 返回 true", bad)
}
if _, err := bcryptx.Cost(bad); err == nil {
t.Errorf("Cost(%q) 期望错误", bad)
}
}
}
func TestNeedsRehash(t *testing.T) {
hash, err := bcryptx.HashPasswordWithCost([]byte("pw"), bcryptx.MinCost)
if err != nil {
t.Fatalf("HashPasswordWithCost 失败: %v", err)
}
// 当前代价低于目标 → 需要重算
if !bcryptx.NeedsRehash(hash, bcryptx.DefaultCost) {
t.Error("低代价哈希应当需要重算")
}
// 当前代价等于目标 → 不需要
if bcryptx.NeedsRehash(hash, bcryptx.MinCost) {
t.Error("代价已达标不应需要重算")
}
// 无法解析的哈希 → 视为需要重算
if !bcryptx.NeedsRehash("garbage", bcryptx.DefaultCost) {
t.Error("非法哈希应当需要重算")
2025-11-09 18:23:08 +08:00
}
}