mirror of
https://github.com/lejianwen/rustdesk-api.git
synced 2026-02-17 22:08:29 +08: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_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_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_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----- | ---------- | ---------- |
|
| ----- ADMIN Configuration----- | ---------- | ---------- |
|
||||||
| RUSTDESK_API_ADMIN_TITLE | Admin Title | `RustDesk Api Admin` |
|
| RUSTDESK_API_ADMIN_TITLE | Admin Title | `RustDesk Api Admin` |
|
||||||
| RUSTDESK_API_ADMIN_HELLO | Admin welcome message, you can use `html` | |
|
| RUSTDESK_API_ADMIN_HELLO | Admin welcome message, you can use `html` | |
|
||||||
|
|||||||
@@ -342,7 +342,11 @@ func Migrate(version uint) {
|
|||||||
// 生成随机密码
|
// 生成随机密码
|
||||||
pwd := utils.RandomString(8)
|
pwd := utils.RandomString(8)
|
||||||
global.Logger.Info("Admin Password Is: ", pwd)
|
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)
|
global.DB.Create(admin)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
adResp "github.com/lejianwen/rustdesk-api/v2/http/response/admin"
|
adResp "github.com/lejianwen/rustdesk-api/v2/http/response/admin"
|
||||||
"github.com/lejianwen/rustdesk-api/v2/model"
|
"github.com/lejianwen/rustdesk-api/v2/model"
|
||||||
"github.com/lejianwen/rustdesk-api/v2/service"
|
"github.com/lejianwen/rustdesk-api/v2/service"
|
||||||
|
"github.com/lejianwen/rustdesk-api/v2/utils"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
@@ -243,11 +244,10 @@ func (ct *User) ChangeCurPwd(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
u := service.AllService.UserService.CurUser(c)
|
u := service.AllService.UserService.CurUser(c)
|
||||||
// If the password is not empty, the old password is verified
|
// Verify the old password only when the account already has one set
|
||||||
// otherwise, the old password is not verified
|
|
||||||
if !service.AllService.UserService.IsPasswordEmptyByUser(u) {
|
if !service.AllService.UserService.IsPasswordEmptyByUser(u) {
|
||||||
oldPwd := service.AllService.UserService.EncryptPassword(f.OldPassword)
|
ok, _, err := utils.VerifyPassword(u.Password, f.OldPassword)
|
||||||
if u.Password != oldPwd {
|
if err != nil || !ok {
|
||||||
response.Fail(c, 101, response.TranslateMsg(c, "OldPasswordError"))
|
response.Fail(c, 101, response.TranslateMsg(c, "OldPasswordError"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,7 +55,18 @@ func (us *UserService) InfoByUsernamePassword(username, password string) *model.
|
|||||||
Logger.Warn("Fallback to local database")
|
Logger.Warn("Fallback to local database")
|
||||||
}
|
}
|
||||||
u := &model.User{}
|
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
|
return u
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,11 +162,6 @@ func (us *UserService) ListIdAndNameByGroupId(groupId uint) (res []*model.User)
|
|||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
// EncryptPassword 加密密码
|
|
||||||
func (us *UserService) EncryptPassword(password string) string {
|
|
||||||
return utils.Md5(password + "rustdesk-api")
|
|
||||||
}
|
|
||||||
|
|
||||||
// CheckUserEnable 判断用户是否禁用
|
// CheckUserEnable 判断用户是否禁用
|
||||||
func (us *UserService) CheckUserEnable(u *model.User) bool {
|
func (us *UserService) CheckUserEnable(u *model.User) bool {
|
||||||
return u.Status == model.COMMON_STATUS_ENABLE
|
return u.Status == model.COMMON_STATUS_ENABLE
|
||||||
@@ -168,7 +174,11 @@ func (us *UserService) Create(u *model.User) error {
|
|||||||
return errors.New("UsernameExists")
|
return errors.New("UsernameExists")
|
||||||
}
|
}
|
||||||
u.Username = us.formatUsername(u.Username)
|
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
|
res := DB.Create(u).Error
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
@@ -268,8 +278,12 @@ func (us *UserService) FlushTokenByUuids(uuids []string) error {
|
|||||||
|
|
||||||
// UpdatePassword 更新密码
|
// UpdatePassword 更新密码
|
||||||
func (us *UserService) UpdatePassword(u *model.User, password string) error {
|
func (us *UserService) UpdatePassword(u *model.User, password string) error {
|
||||||
u.Password = us.EncryptPassword(password)
|
var err error
|
||||||
err := DB.Model(u).Update("password", u.Password).Error
|
u.Password, err = utils.EncryptPassword(password)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = DB.Model(u).Update("password", u.Password).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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