Skip to content

Commit 8d9d6aa

Browse files
EpicCodertechknowlogick
authored andcommitted
Add additional password hash algorithms (closes #5859) (#6023)
1 parent 1b85b24 commit 8d9d6aa

23 files changed

+2898
-23
lines changed

custom/conf/app.ini.sample

+2
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,8 @@ MIN_PASSWORD_LENGTH = 6
319319
IMPORT_LOCAL_PATHS = false
320320
; Set to true to prevent all users (including admin) from creating custom git hooks
321321
DISABLE_GIT_HOOKS = false
322+
; Password Hash algorithm, either "pbkdf2", "argon2", "scrypt" or "bcrypt"
323+
PASSWORD_HASH_ALGO = pbkdf2
322324

323325
[openid]
324326
;

docs/content/doc/advanced/config-cheat-sheet.en-us.md

+1
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
197197
- `IMPORT_LOCAL_PATHS`: **false**: Set to `false` to prevent all users (including admin) from importing local path on server.
198198
- `INTERNAL_TOKEN`: **\<random at every install if no uri set\>**: Secret used to validate communication within Gitea binary.
199199
- `INTERNAL_TOKEN_URI`: **<empty>**: Instead of defining internal token in the configuration, this configuration option can be used to give Gitea a path to a file that contains the internal token (example value: `file:/etc/gitea/internal_token`)
200+
- `PASSWORD_HASH_ALGO`: **pbkdf2**: The hash algorithm to use \[pbkdf2, argon2, scrypt, bcrypt\].
200201

201202
## OpenID (`openid`)
202203

models/login_source.go

+10
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"code.gitea.io/gitea/modules/auth/oauth2"
2323
"code.gitea.io/gitea/modules/auth/pam"
2424
"code.gitea.io/gitea/modules/log"
25+
"code.gitea.io/gitea/modules/setting"
2526
"code.gitea.io/gitea/modules/util"
2627
)
2728

@@ -665,6 +666,15 @@ func UserSignIn(username, password string) (*User, error) {
665666
switch user.LoginType {
666667
case LoginNoType, LoginPlain, LoginOAuth2:
667668
if user.IsPasswordSet() && user.ValidatePassword(password) {
669+
670+
// Update password hash if server password hash algorithm have changed
671+
if user.PasswdHashAlgo != setting.PasswordHashAlgo {
672+
user.HashPassword(password)
673+
if err := UpdateUserCols(user, "passwd", "passwd_hash_algo"); err != nil {
674+
return nil, err
675+
}
676+
}
677+
668678
// WARN: DON'T check user.IsActive, that will be checked on reqSign so that
669679
// user could be hint to resend confirm email.
670680
if user.ProhibitLogin {

models/user.go

+41-7
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,10 @@ import (
3333

3434
"github.com/Unknwon/com"
3535
"github.com/go-xorm/xorm"
36+
"golang.org/x/crypto/argon2"
37+
"golang.org/x/crypto/bcrypt"
3638
"golang.org/x/crypto/pbkdf2"
39+
"golang.org/x/crypto/scrypt"
3740
"golang.org/x/crypto/ssh"
3841
"xorm.io/builder"
3942
"xorm.io/core"
@@ -50,6 +53,13 @@ const (
5053
UserTypeOrganization
5154
)
5255

56+
const (
57+
algoBcrypt = "bcrypt"
58+
algoScrypt = "scrypt"
59+
algoArgon2 = "argon2"
60+
algoPbkdf2 = "pbkdf2"
61+
)
62+
5363
const syncExternalUsers = "sync_external_users"
5464

5565
var (
@@ -82,6 +92,7 @@ type User struct {
8292
Email string `xorm:"NOT NULL"`
8393
KeepEmailPrivate bool
8494
Passwd string `xorm:"NOT NULL"`
95+
PasswdHashAlgo string `xorm:"NOT NULL DEFAULT 'pbkdf2'"`
8596

8697
// MustChangePassword is an attribute that determines if a user
8798
// is to change his/her password after registration.
@@ -430,25 +441,48 @@ func (u *User) NewGitSig() *git.Signature {
430441
}
431442
}
432443

433-
func hashPassword(passwd, salt string) string {
434-
tempPasswd := pbkdf2.Key([]byte(passwd), []byte(salt), 10000, 50, sha256.New)
444+
func hashPassword(passwd, salt, algo string) string {
445+
var tempPasswd []byte
446+
447+
switch algo {
448+
case algoBcrypt:
449+
tempPasswd, _ = bcrypt.GenerateFromPassword([]byte(passwd), bcrypt.DefaultCost)
450+
return string(tempPasswd)
451+
case algoScrypt:
452+
tempPasswd, _ = scrypt.Key([]byte(passwd), []byte(salt), 65536, 16, 2, 50)
453+
case algoArgon2:
454+
tempPasswd = argon2.IDKey([]byte(passwd), []byte(salt), 2, 65536, 8, 50)
455+
case algoPbkdf2:
456+
fallthrough
457+
default:
458+
tempPasswd = pbkdf2.Key([]byte(passwd), []byte(salt), 10000, 50, sha256.New)
459+
}
460+
435461
return fmt.Sprintf("%x", tempPasswd)
436462
}
437463

438-
// HashPassword hashes a password using PBKDF.
464+
// HashPassword hashes a password using the algorithm defined in the config value of PASSWORD_HASH_ALGO.
439465
func (u *User) HashPassword(passwd string) {
440-
u.Passwd = hashPassword(passwd, u.Salt)
466+
u.PasswdHashAlgo = setting.PasswordHashAlgo
467+
u.Passwd = hashPassword(passwd, u.Salt, setting.PasswordHashAlgo)
441468
}
442469

443470
// ValidatePassword checks if given password matches the one belongs to the user.
444471
func (u *User) ValidatePassword(passwd string) bool {
445-
tempHash := hashPassword(passwd, u.Salt)
446-
return subtle.ConstantTimeCompare([]byte(u.Passwd), []byte(tempHash)) == 1
472+
tempHash := hashPassword(passwd, u.Salt, u.PasswdHashAlgo)
473+
474+
if u.PasswdHashAlgo != algoBcrypt && subtle.ConstantTimeCompare([]byte(u.Passwd), []byte(tempHash)) == 1 {
475+
return true
476+
}
477+
if u.PasswdHashAlgo == algoBcrypt && bcrypt.CompareHashAndPassword([]byte(u.Passwd), []byte(passwd)) == nil {
478+
return true
479+
}
480+
return false
447481
}
448482

449483
// IsPasswordSet checks if the password is set or left empty
450484
func (u *User) IsPasswordSet() bool {
451-
return !u.ValidatePassword("")
485+
return len(u.Passwd) > 0
452486
}
453487

454488
// UploadAvatar saves custom avatar for user.

models/user_test.go

+23-15
Original file line numberDiff line numberDiff line change
@@ -147,21 +147,29 @@ func TestHashPasswordDeterministic(t *testing.T) {
147147
b := make([]byte, 16)
148148
rand.Read(b)
149149
u := &User{Salt: string(b)}
150-
for i := 0; i < 50; i++ {
151-
// generate a random password
152-
rand.Read(b)
153-
pass := string(b)
154-
155-
// save the current password in the user - hash it and store the result
156-
u.HashPassword(pass)
157-
r1 := u.Passwd
158-
159-
// run again
160-
u.HashPassword(pass)
161-
r2 := u.Passwd
162-
163-
// assert equal (given the same salt+pass, the same result is produced)
164-
assert.Equal(t, r1, r2)
150+
algos := []string{"pbkdf2", "argon2", "scrypt", "bcrypt"}
151+
for j := 0; j < len(algos); j++ {
152+
u.PasswdHashAlgo = algos[j]
153+
for i := 0; i < 50; i++ {
154+
// generate a random password
155+
rand.Read(b)
156+
pass := string(b)
157+
158+
// save the current password in the user - hash it and store the result
159+
u.HashPassword(pass)
160+
r1 := u.Passwd
161+
162+
// run again
163+
u.HashPassword(pass)
164+
r2 := u.Passwd
165+
166+
// assert equal (given the same salt+pass, the same result is produced) except bcrypt
167+
if u.PasswdHashAlgo == "bcrypt" {
168+
assert.NotEqual(t, r1, r2)
169+
} else {
170+
assert.Equal(t, r1, r2)
171+
}
172+
}
165173
}
166174
}
167175

modules/setting/setting.go

+2
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ var (
154154
MinPasswordLength int
155155
ImportLocalPaths bool
156156
DisableGitHooks bool
157+
PasswordHashAlgo string
157158

158159
// Database settings
159160
UseSQLite3 bool
@@ -779,6 +780,7 @@ func NewContext() {
779780
MinPasswordLength = sec.Key("MIN_PASSWORD_LENGTH").MustInt(6)
780781
ImportLocalPaths = sec.Key("IMPORT_LOCAL_PATHS").MustBool(false)
781782
DisableGitHooks = sec.Key("DISABLE_GIT_HOOKS").MustBool(false)
783+
PasswordHashAlgo = sec.Key("PASSWORD_HASH_ALGO").MustString("pbkdf2")
782784
InternalToken = loadInternalToken(sec)
783785
IterateBufferSize = Cfg.Section("database").Key("ITERATE_BUFFER_SIZE").MustInt(50)
784786
LogSQL = Cfg.Section("database").Key("LOG_SQL").MustBool(true)

0 commit comments

Comments
 (0)