diff --git a/README_EN.md b/README_EN.md index 3a936c0..2ca78c6 100644 --- a/README_EN.md +++ b/README_EN.md @@ -164,7 +164,8 @@ The table below does not list all configurations. Please refer to the configurat | RUSTDESK_API_APP_DISABLE_PWD_LOGIN | disable password login | `false` | | RUSTDESK_API_APP_REGISTER_STATUS | register user default status ; 1 enabled , 2 disabled ; default 1 | `1` | | RUSTDESK_API_APP_CAPTCHA_THRESHOLD | captcha threshold; -1 disabled, 0 always enable, >0 threshold ;default `3` | `3` | -| RUSTDESK_API_APP_BAN_THRESHOLD | ban ip threshold; 0 disabled, >0 threshold ; default `0` | `0` | +| RUSTDESK_API_APP_BAN_THRESHOLD | ban ip threshold; 0 disabled, >0 threshold ; default `0` + | `0` | | ----- ADMIN Configuration----- | ---------- | ---------- | | RUSTDESK_API_ADMIN_TITLE | Admin Title | `RustDesk Api Admin` | | RUSTDESK_API_ADMIN_HELLO | Admin welcome message, you can use `html` | | diff --git a/cmd/apimain.go b/cmd/apimain.go index 54016b4..3d402d4 100644 --- a/cmd/apimain.go +++ b/cmd/apimain.go @@ -342,7 +342,11 @@ func Migrate(version uint) { // 生成随机密码 pwd := utils.RandomString(8) global.Logger.Info("Admin Password Is: ", pwd) - admin.Password = service.AllService.UserService.EncryptPassword(pwd) + var err error + admin.Password, err = utils.EncryptPassword(pwd) + if err != nil { + global.Logger.Fatalf("failed to generate admin password: %v", err) + } global.DB.Create(admin) } diff --git a/http/controller/admin/user.go b/http/controller/admin/user.go index 4b29530..963b1cd 100644 --- a/http/controller/admin/user.go +++ b/http/controller/admin/user.go @@ -8,6 +8,7 @@ import ( adResp "github.com/lejianwen/rustdesk-api/v2/http/response/admin" "github.com/lejianwen/rustdesk-api/v2/model" "github.com/lejianwen/rustdesk-api/v2/service" + "github.com/lejianwen/rustdesk-api/v2/utils" "gorm.io/gorm" "strconv" ) @@ -243,11 +244,10 @@ func (ct *User) ChangeCurPwd(c *gin.Context) { return } u := service.AllService.UserService.CurUser(c) - // If the password is not empty, the old password is verified - // otherwise, the old password is not verified + // Verify the old password only when the account already has one set if !service.AllService.UserService.IsPasswordEmptyByUser(u) { - oldPwd := service.AllService.UserService.EncryptPassword(f.OldPassword) - if u.Password != oldPwd { + ok, _, err := utils.VerifyPassword(u.Password, f.OldPassword) + if err != nil || !ok { response.Fail(c, 101, response.TranslateMsg(c, "OldPasswordError")) return } diff --git a/service/user.go b/service/user.go index aca25e9..972be89 100644 --- a/service/user.go +++ b/service/user.go @@ -55,7 +55,18 @@ func (us *UserService) InfoByUsernamePassword(username, password string) *model. Logger.Warn("Fallback to local database") } u := &model.User{} - DB.Where("username = ? and password = ?", username, us.EncryptPassword(password)).First(u) + DB.Where("username = ?", username).First(u) + if u.Id == 0 { + return u + } + ok, newHash, err := utils.VerifyPassword(u.Password, password) + if err != nil || !ok { + return &model.User{} + } + if newHash != "" { + DB.Model(u).Update("password", newHash) + u.Password = newHash + } return u } @@ -151,11 +162,6 @@ func (us *UserService) ListIdAndNameByGroupId(groupId uint) (res []*model.User) return res } -// EncryptPassword 加密密码 -func (us *UserService) EncryptPassword(password string) string { - return utils.Md5(password + "rustdesk-api") -} - // CheckUserEnable 判断用户是否禁用 func (us *UserService) CheckUserEnable(u *model.User) bool { return u.Status == model.COMMON_STATUS_ENABLE @@ -168,7 +174,11 @@ func (us *UserService) Create(u *model.User) error { return errors.New("UsernameExists") } u.Username = us.formatUsername(u.Username) - u.Password = us.EncryptPassword(u.Password) + var err error + u.Password, err = utils.EncryptPassword(u.Password) + if err != nil { + return err + } res := DB.Create(u).Error return res } @@ -268,8 +278,12 @@ func (us *UserService) FlushTokenByUuids(uuids []string) error { // UpdatePassword 更新密码 func (us *UserService) UpdatePassword(u *model.User, password string) error { - u.Password = us.EncryptPassword(password) - err := DB.Model(u).Update("password", u.Password).Error + var err error + u.Password, err = utils.EncryptPassword(password) + if err != nil { + return err + } + err = DB.Model(u).Update("password", u.Password).Error if err != nil { return err } diff --git a/utils/password.go b/utils/password.go new file mode 100644 index 0000000..5573761 --- /dev/null +++ b/utils/password.go @@ -0,0 +1,42 @@ +package utils + +import ( + "errors" + "golang.org/x/crypto/bcrypt" +) + +// EncryptPassword hashes the input password using bcrypt. +// An error is returned if hashing fails. +func EncryptPassword(password string) (string, error) { + bs, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return "", err + } + return string(bs), nil +} + +// VerifyPassword checks the input password against the stored hash. +// When a legacy MD5 hash is provided, the password is rehashed with bcrypt +// and the new hash is returned. Any internal bcrypt error is returned. +func VerifyPassword(hash, input string) (bool, string, error) { + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(input)) + if err == nil { + return true, "", nil + } + + var invalidPrefixErr bcrypt.InvalidHashPrefixError + if errors.As(err, &invalidPrefixErr) || errors.Is(err, bcrypt.ErrHashTooShort) { + // Try fallback to legacy MD5 hash verification + if hash == Md5(input+"rustdesk-api") { + newHash, err2 := bcrypt.GenerateFromPassword([]byte(input), bcrypt.DefaultCost) + if err2 != nil { + return true, "", err2 + } + return true, string(newHash), nil + } + } + if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) { + return false, "", nil + } + return false, "", err +} diff --git a/utils/password_test.go b/utils/password_test.go new file mode 100644 index 0000000..43704cc --- /dev/null +++ b/utils/password_test.go @@ -0,0 +1,40 @@ +package utils + +import ( + "testing" + + "golang.org/x/crypto/bcrypt" +) + +func TestVerifyPasswordMD5(t *testing.T) { + hash := Md5("secret" + "rustdesk-api") + ok, newHash, err := VerifyPassword(hash, "secret") + if err != nil { + t.Fatalf("md5 verify failed: %v", err) + } + if !ok || newHash == "" { + t.Fatalf("md5 migration failed") + } + if bcrypt.CompareHashAndPassword([]byte(newHash), []byte("secret")) != nil { + t.Fatalf("invalid rehash") + } +} + +func TestVerifyPasswordBcrypt(t *testing.T) { + b, _ := bcrypt.GenerateFromPassword([]byte("pass"), bcrypt.DefaultCost) + ok, newHash, err := VerifyPassword(string(b), "pass") + if err != nil || !ok || newHash != "" { + t.Fatalf("bcrypt verify failed") + } +} + +func TestVerifyPasswordMigrate(t *testing.T) { + md5hash := Md5("mypass" + "rustdesk-api") + ok, newHash, err := VerifyPassword(md5hash, "mypass") + if err != nil || !ok || newHash == "" { + t.Fatalf("expected bcrypt rehash") + } + if bcrypt.CompareHashAndPassword([]byte(newHash), []byte("mypass")) != nil { + t.Fatalf("rehash not valid bcrypt") + } +}