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("不同备份码不应交叉匹配") } }