From 1c00d64952bb18476961a71545598e1ecc7726d0 Mon Sep 17 00:00:00 2001 From: Andy Hsu Date: Mon, 7 Aug 2023 15:46:19 +0800 Subject: [PATCH] feat: rehash password with a unique salt for each user --- cmd/admin.go | 3 +- internal/bootstrap/data/user.go | 10 +++-- internal/model/user.go | 66 ++++++++++++++++++++------------- server/handles/auth.go | 4 +- server/webdav.go | 2 +- 5 files changed, 52 insertions(+), 33 deletions(-) diff --git a/cmd/admin.go b/cmd/admin.go index 8ac9b99b..5dab2b25 100644 --- a/cmd/admin.go +++ b/cmd/admin.go @@ -5,7 +5,6 @@ package cmd import ( "github.com/alist-org/alist/v3/internal/conf" - "github.com/alist-org/alist/v3/internal/model" "github.com/alist-org/alist/v3/internal/op" "github.com/alist-org/alist/v3/internal/setting" "github.com/alist-org/alist/v3/pkg/utils" @@ -70,7 +69,7 @@ func setAdminPassword(pwd string) { utils.Log.Errorf("failed get admin user: %+v", err) return } - admin.PwdHash = model.HashPwd(pwd) + admin.SetPassword(pwd) if err := op.UpdateUser(admin); err != nil { utils.Log.Errorf("failed update admin user: %+v", err) return diff --git a/internal/bootstrap/data/user.go b/internal/bootstrap/data/user.go index b12d32e5..9ac62fe8 100644 --- a/internal/bootstrap/data/user.go +++ b/internal/bootstrap/data/user.go @@ -24,9 +24,11 @@ func initUser() { } if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { + salt := random.String(16) admin = &model.User{ Username: "admin", - PwdHash: model.HashPwd(adminPassword), + Salt: salt, + PwdHash: model.TwoHashPwd(adminPassword, salt), Role: model.ADMIN, BasePath: "/", } @@ -42,9 +44,10 @@ func initUser() { guest, err := op.GetGuest() if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { + salt := random.String(16) guest = &model.User{ Username: "guest", - PwdHash: model.HashPwd("guest"), + PwdHash: model.TwoHashPwd("guest", salt), Role: model.GUEST, BasePath: "/", Permission: 0, @@ -68,7 +71,8 @@ func hashPwdForOldVersion() { for i := range users { user := users[i] if user.PwdHash == "" { - user.PwdHash = model.HashPwd(user.Password) + user.SetPassword(user.Password) + user.Password = "" if err := db.UpdateUser(&user); err != nil { utils.Log.Fatalf("[hash pwd for old version] failed update user: %v", err) } diff --git a/internal/model/user.go b/internal/model/user.go index b276cc60..a7236135 100644 --- a/internal/model/user.go +++ b/internal/model/user.go @@ -5,6 +5,7 @@ import ( "github.com/alist-org/alist/v3/internal/errs" "github.com/alist-org/alist/v3/pkg/utils" + "github.com/alist-org/alist/v3/pkg/utils/random" "github.com/pkg/errors" ) @@ -14,15 +15,16 @@ const ( ADMIN ) -const HashSalt = "https://github.com/alist-org/alist" +const StaticHashSalt = "https://github.com/alist-org/alist" type User struct { ID uint `json:"id" gorm:"primaryKey"` // unique key Username string `json:"username" gorm:"unique" binding:"required"` // username PwdHash string `json:"-"` // password hash - Password string `json:"-"` // password - BasePath string `json:"base_path"` // base path - Role int `json:"role"` // user's role + Salt string // unique salt + Password string `json:"-"` // Deprecated password + BasePath string `json:"base_path"` // base path + Role int `json:"role"` // user's role Disabled bool `json:"disabled"` // Determine permissions by bit // 0: can see hidden files @@ -41,76 +43,90 @@ type User struct { SsoID string `json:"sso_id"` // unique by sso platform } -func (u User) IsGuest() bool { +func (u *User) IsGuest() bool { return u.Role == GUEST } -func (u User) IsAdmin() bool { +func (u *User) IsAdmin() bool { return u.Role == ADMIN } -func (u User) ValidatePassword(password string) error { - return u.ValidatePwdHash(HashPwd(password)) +func (u *User) ValidateRawPassword(password string) error { + return u.ValidatePwdStaticHash(StaticHash(password)) } -func (u User) ValidatePwdHash(pwdHash string) error { - if pwdHash == "" { +func (u *User) ValidatePwdStaticHash(pwdStaticHash string) error { + if pwdStaticHash == "" { return errors.WithStack(errs.EmptyPassword) } - if u.PwdHash != pwdHash { + if u.PwdHash != HashPwd(pwdStaticHash, u.Salt) { return errors.WithStack(errs.WrongPassword) } return nil } -func (u User) CanSeeHides() bool { +func (u *User) SetPassword(pwd string) *User { + u.Salt = random.String(16) + u.PwdHash = TwoHashPwd(pwd, u.Salt) + return u +} + +func (u *User) CanSeeHides() bool { return u.IsAdmin() || u.Permission&1 == 1 } -func (u User) CanAccessWithoutPassword() bool { +func (u *User) CanAccessWithoutPassword() bool { return u.IsAdmin() || (u.Permission>>1)&1 == 1 } -func (u User) CanAddAria2Tasks() bool { +func (u *User) CanAddAria2Tasks() bool { return u.IsAdmin() || (u.Permission>>2)&1 == 1 } -func (u User) CanWrite() bool { +func (u *User) CanWrite() bool { return u.IsAdmin() || (u.Permission>>3)&1 == 1 } -func (u User) CanRename() bool { +func (u *User) CanRename() bool { return u.IsAdmin() || (u.Permission>>4)&1 == 1 } -func (u User) CanMove() bool { +func (u *User) CanMove() bool { return u.IsAdmin() || (u.Permission>>5)&1 == 1 } -func (u User) CanCopy() bool { +func (u *User) CanCopy() bool { return u.IsAdmin() || (u.Permission>>6)&1 == 1 } -func (u User) CanRemove() bool { +func (u *User) CanRemove() bool { return u.IsAdmin() || (u.Permission>>7)&1 == 1 } -func (u User) CanWebdavRead() bool { +func (u *User) CanWebdavRead() bool { return u.IsAdmin() || (u.Permission>>8)&1 == 1 } -func (u User) CanWebdavManage() bool { +func (u *User) CanWebdavManage() bool { return u.IsAdmin() || (u.Permission>>9)&1 == 1 } -func (u User) CanAddQbittorrentTasks() bool { +func (u *User) CanAddQbittorrentTasks() bool { return u.IsAdmin() || (u.Permission>>10)&1 == 1 } -func (u User) JoinPath(reqPath string) (string, error) { +func (u *User) JoinPath(reqPath string) (string, error) { return utils.JoinBasePath(u.BasePath, reqPath) } -func HashPwd(password string) string { - return utils.GetSHA256Encode([]byte(fmt.Sprintf("%s-%s", password, HashSalt))) +func StaticHash(password string) string { + return utils.GetSHA256Encode([]byte(fmt.Sprintf("%s-%s", password, StaticHashSalt))) +} + +func HashPwd(static string, salt string) string { + return utils.GetSHA256Encode([]byte(fmt.Sprintf("%s-%s", static, salt))) +} + +func TwoHashPwd(password string, salt string) string { + return HashPwd(StaticHash(password), salt) } diff --git a/server/handles/auth.go b/server/handles/auth.go index 4e140d72..235a575e 100644 --- a/server/handles/auth.go +++ b/server/handles/auth.go @@ -33,7 +33,7 @@ func Login(c *gin.Context) { common.ErrorResp(c, err, 400) return } - req.Password = model.HashPwd(req.Password) + req.Password = model.StaticHash(req.Password) loginHash(c, &req) } @@ -64,7 +64,7 @@ func loginHash(c *gin.Context, req *LoginReq) { return } // validate password hash - if err := user.ValidatePwdHash(req.Password); err != nil { + if err := user.ValidatePwdStaticHash(req.Password); err != nil { common.ErrorResp(c, err, 400) loginCache.Set(ip, count+1) return diff --git a/server/webdav.go b/server/webdav.go index 1ddaaf73..8d5fd91a 100644 --- a/server/webdav.go +++ b/server/webdav.go @@ -78,7 +78,7 @@ func WebDAVAuth(c *gin.Context) { return } user, err := op.GetUserByName(username) - if err != nil || user.ValidatePassword(password) != nil { + if err != nil || user.ValidateRawPassword(password) != nil { if c.Request.Method == "OPTIONS" { c.Set("user", guest) c.Next()