c949204662
借鉴 odysseus 的能力设计,全程净室实现、零 AGPL 代码、不引入 AGPL 依赖。
T1 提示注入防护: pkg/promptguard 包裹外部/知识库内容为不可信数据,buildMessages 移出 system 指令区。 T2 安全 CI: .github/workflows(ci+security: govulncheck/gitleaks/actionlint/hadolint/trivy)+dependabot+.hadolint.yaml;go.mod 加 toolchain go1.25.11 修复 20 个 stdlib CVE。 T3 管理员 2FA: 迁移 000016 + RFC6238 TOTP/备份码(pkg/auth, 零依赖) + 登录流程集成(后端)。 T4 本地模型: LLM/embedding 支持本地 vLLM/Ollama(OpenAI 兼容, 鉴权头条件发送, NoAuth) + docs/local-deploy.md。 T6 深度研究: 迁移 000017 + Python research-worker(净室多步流水线, 检索避开 SearXNG) + Go research 服务/handler/路由。 T7 service 层: 新增 internal/service/{research,twofa}, 2FA 业务逻辑从胖 handler 下沉, 接口注入可单测。 T10 缓存/可观测性: internal/cache(Redis+内存, 优雅降级) 接入 store 热点列表; Prometheus 指标+/metrics; docs/openapi.yaml。 验证: go build/vet/test ./... 全绿(8 包); research-worker 12 单测过; 真实 PG 应用迁移并烟测。
91 lines
2.8 KiB
Go
91 lines
2.8 KiB
Go
package auth
|
||
|
||
import (
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
// RFC 6238 测试向量:种子 ASCII "12345678901234567890"(base32 如下),
|
||
// SHA1、time=59s 对应 8 位 TOTP 为 94287082,截断到 6 位即 287082。
|
||
func TestTOTP_RFC6238Vector(t *testing.T) {
|
||
const secret = "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ" // base32("12345678901234567890")
|
||
code, err := TOTPCodeAt(secret, 59)
|
||
if err != nil {
|
||
t.Fatalf("TOTPCodeAt 出错: %v", err)
|
||
}
|
||
if code != "287082" {
|
||
t.Fatalf("RFC6238 向量不匹配:want 287082, got %s", code)
|
||
}
|
||
}
|
||
|
||
func TestValidateTOTP_CurrentAndSkew(t *testing.T) {
|
||
secret, err := GenerateTOTPSecret()
|
||
if err != nil {
|
||
t.Fatalf("生成密钥失败: %v", err)
|
||
}
|
||
var now int64 = 1_700_000_000
|
||
|
||
cur, _ := TOTPCodeAt(secret, now)
|
||
if !ValidateTOTP(secret, cur, now) {
|
||
t.Fatal("当前时间窗的口令应通过校验")
|
||
}
|
||
// 上一个时间窗的口令应在 ±1 窗容忍范围内通过
|
||
prev, _ := TOTPCodeAt(secret, now-30)
|
||
if !ValidateTOTP(secret, prev, now) {
|
||
t.Fatal("上一个时间窗的口令应在容忍范围内通过")
|
||
}
|
||
// 超出 ±1 窗(-90s)应失败
|
||
old, _ := TOTPCodeAt(secret, now-90)
|
||
if ValidateTOTP(secret, old, now) {
|
||
t.Fatal("超出容忍范围的口令应被拒绝")
|
||
}
|
||
// 明显错误的口令应失败
|
||
if ValidateTOTP(secret, "000000", now) && cur != "000000" {
|
||
t.Fatal("错误口令应被拒绝")
|
||
}
|
||
// 长度不符应直接拒绝
|
||
if ValidateTOTP(secret, "12345", now) {
|
||
t.Fatal("位数不足的口令应被拒绝")
|
||
}
|
||
}
|
||
|
||
func TestProvisioningURI(t *testing.T) {
|
||
uri := TOTPProvisioningURI("ABC234", "admin@govai.gov.cn", "政智通 GovAI")
|
||
for _, want := range []string{"otpauth://totp/", "secret=ABC234", "issuer=", "digits=6", "period=30"} {
|
||
if !strings.Contains(uri, want) {
|
||
t.Fatalf("otpauth URI 缺少 %q: %s", want, uri)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestBackupCodes_GenerateVerifyConsumeSemantics(t *testing.T) {
|
||
plain, hashes, err := GenerateBackupCodes(8)
|
||
if err != nil {
|
||
t.Fatalf("生成备份码失败: %v", err)
|
||
}
|
||
if len(plain) != 8 || len(hashes) != 8 {
|
||
t.Fatalf("应生成 8 个备份码,实际 plain=%d hashes=%d", len(plain), len(hashes))
|
||
}
|
||
// 每个明文应能匹配其对应哈希
|
||
for i := range plain {
|
||
if !CheckBackupCode(plain[i], hashes[i]) {
|
||
t.Fatalf("备份码 #%d 无法匹配自身哈希", i)
|
||
}
|
||
}
|
||
// 归一化:大小写/连字符/空格不应影响校验
|
||
if !CheckBackupCode(strings.ToUpper(plain[0]), hashes[0]) {
|
||
t.Fatal("大写形式的备份码应仍匹配")
|
||
}
|
||
if !CheckBackupCode(strings.ReplaceAll(plain[0], "-", ""), hashes[0]) {
|
||
t.Fatal("去掉连字符的备份码应仍匹配")
|
||
}
|
||
// 不匹配的码应失败
|
||
if CheckBackupCode("wrong-code1", hashes[0]) {
|
||
t.Fatal("错误备份码不应匹配")
|
||
}
|
||
// 备份码之间不应交叉匹配
|
||
if CheckBackupCode(plain[0], hashes[1]) {
|
||
t.Fatal("不同备份码不应交叉匹配")
|
||
}
|
||
}
|