Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add additional password hash algorithms #6023

Merged
merged 3 commits into from
Jul 7, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions custom/conf/app.ini.sample
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,8 @@ MIN_PASSWORD_LENGTH = 6
IMPORT_LOCAL_PATHS = false
; Set to true to prevent all users (including admin) from creating custom git hooks
DISABLE_GIT_HOOKS = false
; Password Hash algorithm, either "pbkdf2", "argon2", "scrypt" or "bcrypt"
PASSWORD_HASH_ALGO = pbkdf2

[openid]
;
Expand Down
1 change: 1 addition & 0 deletions docs/content/doc/advanced/config-cheat-sheet.en-us.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
- `IMPORT_LOCAL_PATHS`: **false**: Set to `false` to prevent all users (including admin) from importing local path on server.
- `INTERNAL_TOKEN`: **\<random at every install if no uri set\>**: Secret used to validate communication within Gitea binary.
- `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`)
- `PASSWORD_HASH_ALGO`: **pbkdf2**: The hash algorithm to use \[pbkdf2, argon2, scrypt, bcrypt\].

## OpenID (`openid`)

Expand Down
10 changes: 10 additions & 0 deletions models/login_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"code.gitea.io/gitea/modules/auth/oauth2"
"code.gitea.io/gitea/modules/auth/pam"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
)

Expand Down Expand Up @@ -665,6 +666,15 @@ func UserSignIn(username, password string) (*User, error) {
switch user.LoginType {
case LoginNoType, LoginPlain, LoginOAuth2:
if user.IsPasswordSet() && user.ValidatePassword(password) {

// Update password hash if server password hash algorithm have changed
if user.PasswdHashAlgo != setting.PasswordHashAlgo {
user.HashPassword(password)
if err := UpdateUserCols(user, "passwd", "passwd_hash_algo"); err != nil {
return nil, err
}
}

// WARN: DON'T check user.IsActive, that will be checked on reqSign so that
// user could be hint to resend confirm email.
if user.ProhibitLogin {
Expand Down
48 changes: 41 additions & 7 deletions models/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ import (

"github.com/Unknwon/com"
"github.com/go-xorm/xorm"
"golang.org/x/crypto/argon2"
"golang.org/x/crypto/bcrypt"
"golang.org/x/crypto/pbkdf2"
"golang.org/x/crypto/scrypt"
"golang.org/x/crypto/ssh"
"xorm.io/builder"
"xorm.io/core"
Expand All @@ -50,6 +53,13 @@ const (
UserTypeOrganization
)

const (
algoBcrypt = "bcrypt"
algoScrypt = "scrypt"
algoArgon2 = "argon2"
algoPbkdf2 = "pbkdf2"
)

const syncExternalUsers = "sync_external_users"

var (
Expand Down Expand Up @@ -82,6 +92,7 @@ type User struct {
Email string `xorm:"NOT NULL"`
KeepEmailPrivate bool
Passwd string `xorm:"NOT NULL"`
PasswdHashAlgo string `xorm:"NOT NULL DEFAULT 'pbkdf2'"`

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

func hashPassword(passwd, salt string) string {
tempPasswd := pbkdf2.Key([]byte(passwd), []byte(salt), 10000, 50, sha256.New)
func hashPassword(passwd, salt, algo string) string {
var tempPasswd []byte

switch algo {
case algoBcrypt:
tempPasswd, _ = bcrypt.GenerateFromPassword([]byte(passwd), bcrypt.DefaultCost)
return string(tempPasswd)
case algoScrypt:
tempPasswd, _ = scrypt.Key([]byte(passwd), []byte(salt), 65536, 16, 2, 50)
case algoArgon2:
tempPasswd = argon2.IDKey([]byte(passwd), []byte(salt), 2, 65536, 8, 50)
case algoPbkdf2:
fallthrough
default:
tempPasswd = pbkdf2.Key([]byte(passwd), []byte(salt), 10000, 50, sha256.New)
}

return fmt.Sprintf("%x", tempPasswd)
}

// HashPassword hashes a password using PBKDF.
// HashPassword hashes a password using the algorithm defined in the config value of PASSWORD_HASH_ALGO.
func (u *User) HashPassword(passwd string) {
u.Passwd = hashPassword(passwd, u.Salt)
u.PasswdHashAlgo = setting.PasswordHashAlgo
u.Passwd = hashPassword(passwd, u.Salt, setting.PasswordHashAlgo)
}

// ValidatePassword checks if given password matches the one belongs to the user.
func (u *User) ValidatePassword(passwd string) bool {
tempHash := hashPassword(passwd, u.Salt)
return subtle.ConstantTimeCompare([]byte(u.Passwd), []byte(tempHash)) == 1
tempHash := hashPassword(passwd, u.Salt, u.PasswdHashAlgo)

if u.PasswdHashAlgo != algoBcrypt && subtle.ConstantTimeCompare([]byte(u.Passwd), []byte(tempHash)) == 1 {
return true
}
if u.PasswdHashAlgo == algoBcrypt && bcrypt.CompareHashAndPassword([]byte(u.Passwd), []byte(passwd)) == nil {
return true
}
return false
}

// IsPasswordSet checks if the password is set or left empty
func (u *User) IsPasswordSet() bool {
return !u.ValidatePassword("")
return len(u.Passwd) > 0
}

// UploadAvatar saves custom avatar for user.
Expand Down
38 changes: 23 additions & 15 deletions models/user_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,21 +147,29 @@ func TestHashPasswordDeterministic(t *testing.T) {
b := make([]byte, 16)
rand.Read(b)
u := &User{Salt: string(b)}
for i := 0; i < 50; i++ {
// generate a random password
rand.Read(b)
pass := string(b)

// save the current password in the user - hash it and store the result
u.HashPassword(pass)
r1 := u.Passwd

// run again
u.HashPassword(pass)
r2 := u.Passwd

// assert equal (given the same salt+pass, the same result is produced)
assert.Equal(t, r1, r2)
algos := []string{"pbkdf2", "argon2", "scrypt", "bcrypt"}
for j := 0; j < len(algos); j++ {
u.PasswdHashAlgo = algos[j]
for i := 0; i < 50; i++ {
// generate a random password
rand.Read(b)
pass := string(b)

// save the current password in the user - hash it and store the result
u.HashPassword(pass)
r1 := u.Passwd

// run again
u.HashPassword(pass)
r2 := u.Passwd

// assert equal (given the same salt+pass, the same result is produced) except bcrypt
if u.PasswdHashAlgo == "bcrypt" {
assert.NotEqual(t, r1, r2)
} else {
assert.Equal(t, r1, r2)
}
}
}
}

Expand Down
2 changes: 2 additions & 0 deletions modules/setting/setting.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ var (
MinPasswordLength int
ImportLocalPaths bool
DisableGitHooks bool
PasswordHashAlgo string

// Database settings
UseSQLite3 bool
Expand Down Expand Up @@ -779,6 +780,7 @@ func NewContext() {
MinPasswordLength = sec.Key("MIN_PASSWORD_LENGTH").MustInt(6)
ImportLocalPaths = sec.Key("IMPORT_LOCAL_PATHS").MustBool(false)
DisableGitHooks = sec.Key("DISABLE_GIT_HOOKS").MustBool(false)
PasswordHashAlgo = sec.Key("PASSWORD_HASH_ALGO").MustString("pbkdf2")
InternalToken = loadInternalToken(sec)
IterateBufferSize = Cfg.Section("database").Key("ITERATE_BUFFER_SIZE").MustInt(50)
LogSQL = Cfg.Section("database").Key("LOG_SQL").MustBool(true)
Expand Down
Loading