fix(panel): share TOTP skew tolerance with VerifyTwoFactorCode

Move the +/-1 window helper to internal/util/totp so both 2FA
acceptance points use it: login (CheckUser) and disable/rebind plus
username/password changes (VerifyTwoFactorCode). Also shrink comments
to the 2-line house rule and anchor the unit test mid-window to avoid
a step-boundary flake.

Addresses review on #6546 (MEDIUM + 2 LOWs).
This commit is contained in:
sdhfsl
2026-09-15 18:56:09 +08:00
parent 959e6fd62b
commit f3096bb3c1
6 changed files with 64 additions and 57 deletions
+22
View File
@@ -0,0 +1,22 @@
package totp
import (
"time"
"github.com/xlzd/gotp"
)
// SkewWindows is how many 30s steps around now VerifyWithSkew accepts.
// Standard TOTP clock-drift tolerance, see MHSanaei/3x-ui#6535.
const SkewWindows = 1
// VerifyWithSkew accepts the code for the current step plus/minus SkewWindows.
func VerifyWithSkew(secret, code string, now time.Time) bool {
totp := gotp.NewDefaultTOTP(secret)
for i := -SkewWindows; i <= SkewWindows; i++ {
if totp.AtTime(now.Add(time.Duration(i*30)*time.Second)) == code {
return true
}
}
return false
}
+34
View File
@@ -0,0 +1,34 @@
package totp
import (
"testing"
"time"
"github.com/xlzd/gotp"
)
func TestVerifyWithSkew(t *testing.T) {
secret := "JBSWY3DPEHPK3PXP"
totp := gotp.NewDefaultTOTP(secret)
// Anchor mid-window so a step boundary can't fall between sampling and verify.
now := time.Unix((time.Now().Unix()/30)*30+15, 0).UTC()
if !VerifyWithSkew(secret, totp.AtTime(now), now) {
t.Fatal("current window code should verify")
}
if !VerifyWithSkew(secret, totp.AtTime(now.Add(-30*time.Second)), now) {
t.Fatal("previous window code should verify (clock skew)")
}
if !VerifyWithSkew(secret, totp.AtTime(now.Add(30*time.Second)), now) {
t.Fatal("next window code should verify (clock skew)")
}
if VerifyWithSkew(secret, totp.AtTime(now.Add(-60*time.Second)), now) {
t.Fatal("code two windows old should not verify")
}
if VerifyWithSkew(secret, totp.AtTime(now.Add(60*time.Second)), now) {
t.Fatal("code two windows ahead should not verify")
}
if VerifyWithSkew(secret, "000000", now) {
t.Fatal("wrong code should not verify")
}
}