mirror of
https://github.com/lejianwen/rustdesk-api.git
synced 2025-11-29 08:33:21 +00:00
feat(password): Password hashing with bcrypt (#290)
* feat(password): add configurable password hashing with md5 and bcrypt * docs: add password hashing algorithm configuration (bcrypt/md5) * feat(password): better bcrypt fallback and minor refactoring * feat(password): handle errors in password encryption and verification * feat(password): remove password hashing algorithm configuration
This commit is contained in:
@@ -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` | |
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
42
utils/password.go
Normal file
42
utils/password.go
Normal file
@@ -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
|
||||
}
|
||||
40
utils/password_test.go
Normal file
40
utils/password_test.go
Normal file
@@ -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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user