package bcryptx_test import ( "bytes" "errors" "strings" "testing" "code.yun.ink/pkg/encryptx/bcryptx" ) func TestHashAndVerify(t *testing.T) { password := []byte("correct horse battery staple") hash, err := bcryptx.HashPassword(password) if err != nil { t.Fatalf("HashPassword 失败: %v", err) } 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("非法哈希应当需要重算") } }