mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-13 08:35:00 +03:00
7367ff75cd
gotp decodes the stored secret with strict base32 and panics when that fails, so a 2FA secret saved through the settings API in lower case, grouped by spaces, or not base32 at all turned every login and every attempt to turn 2FA off into an HTTP 500. An empty secret had the opposite problem: the login check accepted the code of an empty HMAC key, which anyone can compute. VerifyWithSkew now normalizes the secret and verifies nothing for an empty or non-base32 one, so secrets already stored in a display form work again. Turning 2FA on through the settings update stores the normalized secret and refuses one no authenticator could match.
113 lines
3.8 KiB
Go
113 lines
3.8 KiB
Go
package panel
|
|
|
|
import (
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/xlzd/gotp"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database/dbtest"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/util/crypto"
|
|
)
|
|
|
|
// install.sh replaces the credentials only while this reports true, and before
|
|
// the first start a database holds no user at all.
|
|
func TestHasDefaultCredential(t *testing.T) {
|
|
hash := func(t *testing.T, password string) string {
|
|
t.Helper()
|
|
h, err := crypto.HashPasswordAsBcrypt(password)
|
|
if err != nil {
|
|
t.Fatalf("hash: %v", err)
|
|
}
|
|
return h
|
|
}
|
|
cases := []struct {
|
|
name string
|
|
user *model.User
|
|
want bool
|
|
}{
|
|
{name: "no user before the first start", user: nil, want: true},
|
|
{name: "legacy admin/admin", user: &model.User{Username: "admin", Password: hash(t, "admin")}, want: true},
|
|
{name: "chosen credentials", user: &model.User{Username: "admin", Password: hash(t, "s3cret-Pass")}, want: false},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
dbtest.InitDB(t, filepath.Join(t.TempDir(), "x-ui.db"))
|
|
if err := database.GetDB().Where("1 = 1").Delete(&model.User{}).Error; err != nil {
|
|
t.Fatalf("clear users: %v", err)
|
|
}
|
|
if tc.user != nil {
|
|
if err := database.GetDB().Create(tc.user).Error; err != nil {
|
|
t.Fatalf("create user: %v", err)
|
|
}
|
|
}
|
|
got, err := (&UserService{}).HasDefaultCredential()
|
|
if err != nil || got != tc.want {
|
|
t.Fatalf("HasDefaultCredential() = %v, %v; want %v", got, err, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// The login route refuses names over MaxUsernameLength unread, so storing one would lock
|
|
// the admin out of their own panel.
|
|
func TestUserSettersRefuseNamesTheLoginRouteRefuses(t *testing.T) {
|
|
dbtest.InitDB(t, filepath.Join(t.TempDir(), "x-ui.db"))
|
|
svc := &UserService{}
|
|
if err := svc.UpdateFirstUser("admin", "s3cret-Pass"); err != nil {
|
|
t.Fatalf("UpdateFirstUser: %v", err)
|
|
}
|
|
var user model.User
|
|
if err := database.GetDB().First(&user).Error; err != nil {
|
|
t.Fatalf("load user: %v", err)
|
|
}
|
|
long := strings.Repeat("a", MaxUsernameLength+1)
|
|
if err := svc.UpdateFirstUser(long, "s3cret-Pass"); err == nil {
|
|
t.Fatal("UpdateFirstUser stored a name the login route refuses")
|
|
}
|
|
if err := svc.UpdateUser(user.Id, long, "s3cret-Pass"); err == nil {
|
|
t.Fatal("UpdateUser stored a name the login route refuses")
|
|
}
|
|
var stored model.User
|
|
if err := database.GetDB().First(&stored, user.Id).Error; err != nil || stored.Username != "admin" {
|
|
t.Fatalf("stored username = %q (%v), want it unchanged", stored.Username, err)
|
|
}
|
|
}
|
|
|
|
// A stored 2FA secret in a form gotp cannot decode made every login panic (HTTP 500).
|
|
func TestCheckUserTwoFactorSecretForms(t *testing.T) {
|
|
cases := []struct {
|
|
name, stored, codeSecret string
|
|
want bool
|
|
}{
|
|
{"canonical", "JBSWY3DPEHPK3PXP", "JBSWY3DPEHPK3PXP", true},
|
|
{"lower case", "jbswy3dpehpk3pxp", "JBSWY3DPEHPK3PXP", true},
|
|
{"not base32", "totp-secret", "JBSWY3DPEHPK3PXP", false},
|
|
{"empty secret", "", "", false},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
dbtest.InitDB(t, filepath.Join(t.TempDir(), "x-ui.db"))
|
|
svc := &UserService{}
|
|
if err := svc.UpdateFirstUser("admin", "s3cret-Pass"); err != nil {
|
|
t.Fatalf("UpdateFirstUser: %v", err)
|
|
}
|
|
if err := svc.settingService.SetTwoFactorEnable(true); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := svc.settingService.SetTwoFactorToken(tc.stored); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
code := gotp.NewDefaultTOTP(tc.codeSecret).AtTime(time.Now())
|
|
user, err := svc.CheckUser("admin", "s3cret-Pass", code)
|
|
if got := err == nil && user != nil; got != tc.want {
|
|
t.Fatalf("CheckUser with stored secret %q: ok = %v (%v), want %v", tc.stored, got, err, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|