0782cf651b
- 用户 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>
76 lines
1.6 KiB
Go
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
|
|
}
|
|
}
|