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:
Plynksiy Nikita
2025-06-24 12:23:36 +03:00
committed by GitHub
parent aa04b225b9
commit 9d2b589faa
6 changed files with 116 additions and 15 deletions

42
utils/password.go Normal file
View 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
View 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")
}
}