Files
pangolin/server/internal/auth/totp_user_test.go
T
wangjia 0782cf651b feat(server): web 用户中心后端(3/3) — 用户 TOTP 2FA + 登录二段式
- 用户 TOTP(auth/totp_user.go,复用 internal/totp + AES-256-GCM 加密存密钥):
  POST /v1/me/totp/setup(生成密钥+otpauth_uri)、/verify(校验码→启用)、
  /disable(校验码→清空)。仅在 USER_TOTP_ENC_KEY(32B/64hex) 配置时挂载。
- 登录二段式:Login 在 totp_enabled 时不发 token,改发短期 pending token(Redis
  5min)+ 返回 {totp_required, pending_token};POST /v1/auth/login/totp 消费
  pending + 校验码 → 发 token。非 TOTP 用户仍走扁平 TokenPair,app 不受影响。
- User 结构 + GetUserByEmail 补 totp_enabled。
- 单测覆盖 AES seal/open 往返 + 篡改/错误密钥检测 + pending token 唯一性。
- 全量 server 23 包测试通过。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 08:45:50 +08:00

76 lines
1.6 KiB
Go

package auth
import (
"crypto/rand"
"testing"
)
func TestSealOpenAES_Roundtrip(t *testing.T) {
var key [32]byte
if _, err := rand.Read(key[:]); err != nil {
t.Fatal(err)
}
const secret = "JBSWY3DPEHPK3PXP" // sample base32 TOTP secret
blob, err := sealAES(key[:], secret)
if err != nil {
t.Fatalf("sealAES: %v", err)
}
if string(blob) == secret {
t.Fatal("ciphertext equals plaintext — not encrypted")
}
got, err := openAES(key[:], blob)
if err != nil {
t.Fatalf("openAES: %v", err)
}
if got != secret {
t.Fatalf("roundtrip mismatch: got %q want %q", got, secret)
}
}
func TestOpenAES_TamperAndWrongKey(t *testing.T) {
var key, other [32]byte
_, _ = rand.Read(key[:])
_, _ = rand.Read(other[:])
blob, err := sealAES(key[:], "secret-value")
if err != nil {
t.Fatal(err)
}
// Wrong key must fail (GCM auth).
if _, err := openAES(other[:], blob); err == nil {
t.Error("openAES with wrong key should fail")
}
// Tampered ciphertext must fail.
bad := append([]byte(nil), blob...)
bad[len(bad)-1] ^= 0xff
if _, err := openAES(key[:], bad); err == nil {
t.Error("openAES with tampered ciphertext should fail")
}
// Too-short blob must fail, not panic.
if _, err := openAES(key[:], []byte{1, 2, 3}); err == nil {
t.Error("openAES with short blob should fail")
}
}
func TestNewToken16_UniqueHex(t *testing.T) {
seen := make(map[string]bool, 100)
for i := 0; i < 100; i++ {
tok, err := newToken16()
if err != nil {
t.Fatal(err)
}
if len(tok) != 32 {
t.Fatalf("token length = %d, want 32 hex chars", len(tok))
}
if seen[tok] {
t.Fatalf("duplicate token: %s", tok)
}
seen[tok] = true
}
}