Просмотр исходного кода

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
Plynksiy Nikita месяцев назад: 7
Родитель
Сommit
9d2b589faa
6 измененных файлов с 116 добавлено и 15 удалено
  1. 2 1
      README_EN.md
  2. 5 1
      cmd/apimain.go
  3. 4 4
      http/controller/admin/user.go
  4. 23 9
      service/user.go
  5. 42 0
      utils/password.go
  6. 40 0
      utils/password_test.go

+ 2 - 1
README_EN.md

@@ -164,7 +164,8 @@ The table below does not list all configurations. Please refer to the configurat
164
 | RUSTDESK_API_APP_DISABLE_PWD_LOGIN                     | disable password login                                                                                                                              | `false`                       |
164
 | RUSTDESK_API_APP_DISABLE_PWD_LOGIN                     | disable password login                                                                                                                              | `false`                       |
165
 | RUSTDESK_API_APP_REGISTER_STATUS                       | register user default status ; 1 enabled , 2 disabled ; default 1                                                                                   | `1`                           |
165
 | RUSTDESK_API_APP_REGISTER_STATUS                       | register user default status ; 1 enabled , 2 disabled ; default 1                                                                                   | `1`                           |
166
 | RUSTDESK_API_APP_CAPTCHA_THRESHOLD                     | captcha threshold; -1 disabled, 0 always enable, >0 threshold  ;default `3`                                                                         | `3`                           |
166
 | RUSTDESK_API_APP_CAPTCHA_THRESHOLD                     | captcha threshold; -1 disabled, 0 always enable, >0 threshold  ;default `3`                                                                         | `3`                           |
167
-| RUSTDESK_API_APP_BAN_THRESHOLD                         | ban ip threshold; 0 disabled, >0 threshold ; default `0`                                                                                            | `0`                           |
167
+| RUSTDESK_API_APP_BAN_THRESHOLD                         | ban ip threshold; 0 disabled, >0 threshold ; default `0`
168
+                                               | `0`                           |
168
 | ----- ADMIN Configuration-----                         | ----------                                                                                                                                          | ----------                    |
169
 | ----- ADMIN Configuration-----                         | ----------                                                                                                                                          | ----------                    |
169
 | RUSTDESK_API_ADMIN_TITLE                               | Admin Title                                                                                                                                         | `RustDesk Api Admin`          |
170
 | RUSTDESK_API_ADMIN_TITLE                               | Admin Title                                                                                                                                         | `RustDesk Api Admin`          |
170
 | RUSTDESK_API_ADMIN_HELLO                               | Admin welcome message, you can use `html`                                                                                                           |                               |
171
 | RUSTDESK_API_ADMIN_HELLO                               | Admin welcome message, you can use `html`                                                                                                           |                               |

+ 5 - 1
cmd/apimain.go

@@ -342,7 +342,11 @@ func Migrate(version uint) {
342
 		// 生成随机密码
342
 		// 生成随机密码
343
 		pwd := utils.RandomString(8)
343
 		pwd := utils.RandomString(8)
344
 		global.Logger.Info("Admin Password Is: ", pwd)
344
 		global.Logger.Info("Admin Password Is: ", pwd)
345
-		admin.Password = service.AllService.UserService.EncryptPassword(pwd)
345
+		var err error
346
+		admin.Password, err = utils.EncryptPassword(pwd)
347
+		if err != nil {
348
+			global.Logger.Fatalf("failed to generate admin password: %v", err)
349
+		}
346
 		global.DB.Create(admin)
350
 		global.DB.Create(admin)
347
 	}
351
 	}
348
 
352
 

+ 4 - 4
http/controller/admin/user.go

@@ -8,6 +8,7 @@ import (
8
 	adResp "github.com/lejianwen/rustdesk-api/v2/http/response/admin"
8
 	adResp "github.com/lejianwen/rustdesk-api/v2/http/response/admin"
9
 	"github.com/lejianwen/rustdesk-api/v2/model"
9
 	"github.com/lejianwen/rustdesk-api/v2/model"
10
 	"github.com/lejianwen/rustdesk-api/v2/service"
10
 	"github.com/lejianwen/rustdesk-api/v2/service"
11
+	"github.com/lejianwen/rustdesk-api/v2/utils"
11
 	"gorm.io/gorm"
12
 	"gorm.io/gorm"
12
 	"strconv"
13
 	"strconv"
13
 )
14
 )
@@ -243,11 +244,10 @@ func (ct *User) ChangeCurPwd(c *gin.Context) {
243
 		return
244
 		return
244
 	}
245
 	}
245
 	u := service.AllService.UserService.CurUser(c)
246
 	u := service.AllService.UserService.CurUser(c)
246
-	// If the password is not empty, the old password is verified
247
-	// otherwise, the old password is not verified
247
+	// Verify the old password only when the account already has one set
248
 	if !service.AllService.UserService.IsPasswordEmptyByUser(u) {
248
 	if !service.AllService.UserService.IsPasswordEmptyByUser(u) {
249
-		oldPwd := service.AllService.UserService.EncryptPassword(f.OldPassword)
250
-		if u.Password != oldPwd {
249
+		ok, _, err := utils.VerifyPassword(u.Password, f.OldPassword)
250
+		if err != nil || !ok {
251
 			response.Fail(c, 101, response.TranslateMsg(c, "OldPasswordError"))
251
 			response.Fail(c, 101, response.TranslateMsg(c, "OldPasswordError"))
252
 			return
252
 			return
253
 		}
253
 		}

+ 23 - 9
service/user.go

@@ -55,7 +55,18 @@ func (us *UserService) InfoByUsernamePassword(username, password string) *model.
55
 		Logger.Warn("Fallback to local database")
55
 		Logger.Warn("Fallback to local database")
56
 	}
56
 	}
57
 	u := &model.User{}
57
 	u := &model.User{}
58
-	DB.Where("username = ? and password = ?", username, us.EncryptPassword(password)).First(u)
58
+	DB.Where("username = ?", username).First(u)
59
+	if u.Id == 0 {
60
+		return u
61
+	}
62
+	ok, newHash, err := utils.VerifyPassword(u.Password, password)
63
+	if err != nil || !ok {
64
+		return &model.User{}
65
+	}
66
+	if newHash != "" {
67
+		DB.Model(u).Update("password", newHash)
68
+		u.Password = newHash
69
+	}
59
 	return u
70
 	return u
60
 }
71
 }
61
 
72
 
@@ -151,11 +162,6 @@ func (us *UserService) ListIdAndNameByGroupId(groupId uint) (res []*model.User)
151
 	return res
162
 	return res
152
 }
163
 }
153
 
164
 
154
-// EncryptPassword 加密密码
155
-func (us *UserService) EncryptPassword(password string) string {
156
-	return utils.Md5(password + "rustdesk-api")
157
-}
158
-
159
 // CheckUserEnable 判断用户是否禁用
165
 // CheckUserEnable 判断用户是否禁用
160
 func (us *UserService) CheckUserEnable(u *model.User) bool {
166
 func (us *UserService) CheckUserEnable(u *model.User) bool {
161
 	return u.Status == model.COMMON_STATUS_ENABLE
167
 	return u.Status == model.COMMON_STATUS_ENABLE
@@ -168,7 +174,11 @@ func (us *UserService) Create(u *model.User) error {
168
 		return errors.New("UsernameExists")
174
 		return errors.New("UsernameExists")
169
 	}
175
 	}
170
 	u.Username = us.formatUsername(u.Username)
176
 	u.Username = us.formatUsername(u.Username)
171
-	u.Password = us.EncryptPassword(u.Password)
177
+	var err error
178
+	u.Password, err = utils.EncryptPassword(u.Password)
179
+	if err != nil {
180
+		return err
181
+	}
172
 	res := DB.Create(u).Error
182
 	res := DB.Create(u).Error
173
 	return res
183
 	return res
174
 }
184
 }
@@ -268,8 +278,12 @@ func (us *UserService) FlushTokenByUuids(uuids []string) error {
268
 
278
 
269
 // UpdatePassword 更新密码
279
 // UpdatePassword 更新密码
270
 func (us *UserService) UpdatePassword(u *model.User, password string) error {
280
 func (us *UserService) UpdatePassword(u *model.User, password string) error {
271
-	u.Password = us.EncryptPassword(password)
272
-	err := DB.Model(u).Update("password", u.Password).Error
281
+	var err error
282
+	u.Password, err = utils.EncryptPassword(password)
283
+	if err != nil {
284
+		return err
285
+	}
286
+	err = DB.Model(u).Update("password", u.Password).Error
273
 	if err != nil {
287
 	if err != nil {
274
 		return err
288
 		return err
275
 	}
289
 	}

+ 42 - 0
utils/password.go

@@ -0,0 +1,42 @@
1
+package utils
2
+
3
+import (
4
+	"errors"
5
+	"golang.org/x/crypto/bcrypt"
6
+)
7
+
8
+// EncryptPassword hashes the input password using bcrypt.
9
+// An error is returned if hashing fails.
10
+func EncryptPassword(password string) (string, error) {
11
+	bs, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
12
+	if err != nil {
13
+		return "", err
14
+	}
15
+	return string(bs), nil
16
+}
17
+
18
+// VerifyPassword checks the input password against the stored hash.
19
+// When a legacy MD5 hash is provided, the password is rehashed with bcrypt
20
+// and the new hash is returned. Any internal bcrypt error is returned.
21
+func VerifyPassword(hash, input string) (bool, string, error) {
22
+	err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(input))
23
+	if err == nil {
24
+		return true, "", nil
25
+	}
26
+
27
+	var invalidPrefixErr bcrypt.InvalidHashPrefixError
28
+	if errors.As(err, &invalidPrefixErr) || errors.Is(err, bcrypt.ErrHashTooShort) {
29
+		// Try fallback to legacy MD5 hash verification
30
+		if hash == Md5(input+"rustdesk-api") {
31
+			newHash, err2 := bcrypt.GenerateFromPassword([]byte(input), bcrypt.DefaultCost)
32
+			if err2 != nil {
33
+				return true, "", err2
34
+			}
35
+			return true, string(newHash), nil
36
+		}
37
+	}
38
+	if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) {
39
+		return false, "", nil
40
+	}
41
+	return false, "", err
42
+}

+ 40 - 0
utils/password_test.go

@@ -0,0 +1,40 @@
1
+package utils
2
+
3
+import (
4
+	"testing"
5
+
6
+	"golang.org/x/crypto/bcrypt"
7
+)
8
+
9
+func TestVerifyPasswordMD5(t *testing.T) {
10
+	hash := Md5("secret" + "rustdesk-api")
11
+	ok, newHash, err := VerifyPassword(hash, "secret")
12
+	if err != nil {
13
+		t.Fatalf("md5 verify failed: %v", err)
14
+	}
15
+	if !ok || newHash == "" {
16
+		t.Fatalf("md5 migration failed")
17
+	}
18
+	if bcrypt.CompareHashAndPassword([]byte(newHash), []byte("secret")) != nil {
19
+		t.Fatalf("invalid rehash")
20
+	}
21
+}
22
+
23
+func TestVerifyPasswordBcrypt(t *testing.T) {
24
+	b, _ := bcrypt.GenerateFromPassword([]byte("pass"), bcrypt.DefaultCost)
25
+	ok, newHash, err := VerifyPassword(string(b), "pass")
26
+	if err != nil || !ok || newHash != "" {
27
+		t.Fatalf("bcrypt verify failed")
28
+	}
29
+}
30
+
31
+func TestVerifyPasswordMigrate(t *testing.T) {
32
+	md5hash := Md5("mypass" + "rustdesk-api")
33
+	ok, newHash, err := VerifyPassword(md5hash, "mypass")
34
+	if err != nil || !ok || newHash == "" {
35
+		t.Fatalf("expected bcrypt rehash")
36
+	}
37
+	if bcrypt.CompareHashAndPassword([]byte(newHash), []byte("mypass")) != nil {
38
+		t.Fatalf("rehash not valid bcrypt")
39
+	}
40
+}