* move SaltGeneration into HashPasswort and rename it to what it does * Migration: Where Password is Valid with Empty String delete it * prohibit empty password hash * let SetPassword("") unset pwd stufftags/v1.15.0-dev
@@ -349,12 +349,11 @@ func runChangePassword(c *cli.Context) error { | |||||
if err != nil { | if err != nil { | ||||
return err | return err | ||||
} | } | ||||
if user.Salt, err = models.GetUserSalt(); err != nil { | |||||
if err = user.SetPassword(c.String("password")); err != nil { | |||||
return err | return err | ||||
} | } | ||||
user.HashPassword(c.String("password")) | |||||
if err := models.UpdateUserCols(user, "passwd", "passwd_hash_algo", "salt"); err != nil { | |||||
if err = models.UpdateUserCols(user, "passwd", "passwd_hash_algo", "salt"); err != nil { | |||||
return err | return err | ||||
} | } | ||||
@@ -771,8 +771,10 @@ func UserSignIn(username, password string) (*User, error) { | |||||
// Update password hash if server password hash algorithm have changed | // Update password hash if server password hash algorithm have changed | ||||
if user.PasswdHashAlgo != setting.PasswordHashAlgo { | if user.PasswdHashAlgo != setting.PasswordHashAlgo { | ||||
user.HashPassword(password) | |||||
if err := UpdateUserCols(user, "passwd", "passwd_hash_algo"); err != nil { | |||||
if err = user.SetPassword(password); err != nil { | |||||
return nil, err | |||||
} | |||||
if err = UpdateUserCols(user, "passwd", "passwd_hash_algo", "salt"); err != nil { | |||||
return nil, err | return nil, err | ||||
} | } | ||||
} | } | ||||
@@ -277,6 +277,8 @@ var migrations = []Migration{ | |||||
NewMigration("Add scope and nonce columns to oauth2_grant table", addScopeAndNonceColumnsToOAuth2Grant), | NewMigration("Add scope and nonce columns to oauth2_grant table", addScopeAndNonceColumnsToOAuth2Grant), | ||||
// v165 -> v166 | // v165 -> v166 | ||||
NewMigration("Convert hook task type from char(16) to varchar(16) and trim the column", convertHookTaskTypeToVarcharAndTrim), | NewMigration("Convert hook task type from char(16) to varchar(16) and trim the column", convertHookTaskTypeToVarcharAndTrim), | ||||
// v166 -> v167 | |||||
NewMigration("Where Password is Valid with Empty String delete it", recalculateUserEmptyPWD), | |||||
} | } | ||||
// GetCurrentDBVersion returns the current db version | // GetCurrentDBVersion returns the current db version | ||||
@@ -0,0 +1,115 @@ | |||||
// Copyright 2021 The Gitea Authors. All rights reserved. | |||||
// Use of this source code is governed by a MIT-style | |||||
// license that can be found in the LICENSE file. | |||||
package migrations | |||||
import ( | |||||
"crypto/sha256" | |||||
"fmt" | |||||
"golang.org/x/crypto/argon2" | |||||
"golang.org/x/crypto/bcrypt" | |||||
"golang.org/x/crypto/pbkdf2" | |||||
"golang.org/x/crypto/scrypt" | |||||
"xorm.io/builder" | |||||
"xorm.io/xorm" | |||||
) | |||||
func recalculateUserEmptyPWD(x *xorm.Engine) (err error) { | |||||
const ( | |||||
algoBcrypt = "bcrypt" | |||||
algoScrypt = "scrypt" | |||||
algoArgon2 = "argon2" | |||||
algoPbkdf2 = "pbkdf2" | |||||
) | |||||
type User struct { | |||||
ID int64 `xorm:"pk autoincr"` | |||||
Passwd string `xorm:"NOT NULL"` | |||||
PasswdHashAlgo string `xorm:"NOT NULL DEFAULT 'argon2'"` | |||||
MustChangePassword bool `xorm:"NOT NULL DEFAULT false"` | |||||
LoginType int | |||||
LoginName string | |||||
Type int | |||||
Salt string `xorm:"VARCHAR(10)"` | |||||
} | |||||
// hashPassword hash password based on algo and salt | |||||
// state 461406070c | |||||
hashPassword := func(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) | |||||
} | |||||
// ValidatePassword checks if given password matches the one belongs to the user. | |||||
// state 461406070c, changed since it's not necessary to be time constant | |||||
ValidatePassword := func(u *User, passwd string) bool { | |||||
tempHash := hashPassword(passwd, u.Salt, u.PasswdHashAlgo) | |||||
if u.PasswdHashAlgo != algoBcrypt && u.Passwd == tempHash { | |||||
return true | |||||
} | |||||
if u.PasswdHashAlgo == algoBcrypt && bcrypt.CompareHashAndPassword([]byte(u.Passwd), []byte(passwd)) == nil { | |||||
return true | |||||
} | |||||
return false | |||||
} | |||||
sess := x.NewSession() | |||||
defer sess.Close() | |||||
const batchSize = 100 | |||||
for start := 0; ; start += batchSize { | |||||
users := make([]*User, 0, batchSize) | |||||
if err = sess.Limit(batchSize, start).Where(builder.Neq{"passwd": ""}, 0).Find(&users); err != nil { | |||||
return | |||||
} | |||||
if len(users) == 0 { | |||||
break | |||||
} | |||||
if err = sess.Begin(); err != nil { | |||||
return | |||||
} | |||||
for _, user := range users { | |||||
if ValidatePassword(user, "") { | |||||
user.Passwd = "" | |||||
user.Salt = "" | |||||
user.PasswdHashAlgo = "" | |||||
if _, err = sess.ID(user.ID).Cols("passwd", "salt", "passwd_hash_algo").Update(user); err != nil { | |||||
return err | |||||
} | |||||
} | |||||
} | |||||
if err = sess.Commit(); err != nil { | |||||
return | |||||
} | |||||
} | |||||
// delete salt and algo where password is empty | |||||
if _, err = sess.Where(builder.Eq{"passwd": ""}.And(builder.Neq{"salt": ""}.Or(builder.Neq{"passwd_hash_algo": ""}))). | |||||
Cols("salt", "passwd_hash_algo").Update(&User{}); err != nil { | |||||
return err | |||||
} | |||||
return sess.Commit() | |||||
} |
@@ -395,10 +395,23 @@ func hashPassword(passwd, salt, algo string) string { | |||||
return fmt.Sprintf("%x", tempPasswd) | return fmt.Sprintf("%x", tempPasswd) | ||||
} | } | ||||
// HashPassword hashes a password using the algorithm defined in the config value of PASSWORD_HASH_ALGO. | |||||
func (u *User) HashPassword(passwd string) { | |||||
// SetPassword hashes a password using the algorithm defined in the config value of PASSWORD_HASH_ALGO | |||||
// change passwd, salt and passwd_hash_algo fields | |||||
func (u *User) SetPassword(passwd string) (err error) { | |||||
if len(passwd) == 0 { | |||||
u.Passwd = "" | |||||
u.Salt = "" | |||||
u.PasswdHashAlgo = "" | |||||
return nil | |||||
} | |||||
if u.Salt, err = GetUserSalt(); err != nil { | |||||
return err | |||||
} | |||||
u.PasswdHashAlgo = setting.PasswordHashAlgo | u.PasswdHashAlgo = setting.PasswordHashAlgo | ||||
u.Passwd = hashPassword(passwd, u.Salt, setting.PasswordHashAlgo) | u.Passwd = hashPassword(passwd, u.Salt, setting.PasswordHashAlgo) | ||||
return nil | |||||
} | } | ||||
// ValidatePassword checks if given password matches the one belongs to the user. | // ValidatePassword checks if given password matches the one belongs to the user. | ||||
@@ -416,7 +429,7 @@ func (u *User) ValidatePassword(passwd string) bool { | |||||
// IsPasswordSet checks if the password is set or left empty | // IsPasswordSet checks if the password is set or left empty | ||||
func (u *User) IsPasswordSet() bool { | func (u *User) IsPasswordSet() bool { | ||||
return !u.ValidatePassword("") | |||||
return len(u.Passwd) != 0 | |||||
} | } | ||||
// IsOrganization returns true if user is actually a organization. | // IsOrganization returns true if user is actually a organization. | ||||
@@ -826,10 +839,9 @@ func CreateUser(u *User) (err error) { | |||||
if u.Rands, err = GetUserSalt(); err != nil { | if u.Rands, err = GetUserSalt(); err != nil { | ||||
return err | return err | ||||
} | } | ||||
if u.Salt, err = GetUserSalt(); err != nil { | |||||
if err = u.SetPassword(u.Passwd); err != nil { | |||||
return err | return err | ||||
} | } | ||||
u.HashPassword(u.Passwd) | |||||
u.AllowCreateOrganization = setting.Service.DefaultAllowCreateOrganization && !setting.Admin.DisableRegularOrgCreation | u.AllowCreateOrganization = setting.Service.DefaultAllowCreateOrganization && !setting.Admin.DisableRegularOrgCreation | ||||
u.EmailNotificationsPreference = setting.Admin.DefaultEmailNotification | u.EmailNotificationsPreference = setting.Admin.DefaultEmailNotification | ||||
u.MaxRepoCreation = -1 | u.MaxRepoCreation = -1 | ||||
@@ -220,8 +220,7 @@ func TestEmailNotificationPreferences(t *testing.T) { | |||||
func TestHashPasswordDeterministic(t *testing.T) { | func TestHashPasswordDeterministic(t *testing.T) { | ||||
b := make([]byte, 16) | b := make([]byte, 16) | ||||
rand.Read(b) | |||||
u := &User{Salt: string(b)} | |||||
u := &User{} | |||||
algos := []string{"argon2", "pbkdf2", "scrypt", "bcrypt"} | algos := []string{"argon2", "pbkdf2", "scrypt", "bcrypt"} | ||||
for j := 0; j < len(algos); j++ { | for j := 0; j < len(algos); j++ { | ||||
u.PasswdHashAlgo = algos[j] | u.PasswdHashAlgo = algos[j] | ||||
@@ -231,19 +230,15 @@ func TestHashPasswordDeterministic(t *testing.T) { | |||||
pass := string(b) | pass := string(b) | ||||
// save the current password in the user - hash it and store the result | // save the current password in the user - hash it and store the result | ||||
u.HashPassword(pass) | |||||
u.SetPassword(pass) | |||||
r1 := u.Passwd | r1 := u.Passwd | ||||
// run again | // run again | ||||
u.HashPassword(pass) | |||||
u.SetPassword(pass) | |||||
r2 := u.Passwd | 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) | |||||
} | |||||
assert.NotEqual(t, r1, r2) | |||||
assert.True(t, u.ValidatePassword(pass)) | |||||
} | } | ||||
} | } | ||||
} | } | ||||
@@ -252,12 +247,10 @@ func BenchmarkHashPassword(b *testing.B) { | |||||
// BenchmarkHashPassword ensures that it takes a reasonable amount of time | // BenchmarkHashPassword ensures that it takes a reasonable amount of time | ||||
// to hash a password - in order to protect from brute-force attacks. | // to hash a password - in order to protect from brute-force attacks. | ||||
pass := "password1337" | pass := "password1337" | ||||
bs := make([]byte, 16) | |||||
rand.Read(bs) | |||||
u := &User{Salt: string(bs), Passwd: pass} | |||||
u := &User{Passwd: pass} | |||||
b.ResetTimer() | b.ResetTimer() | ||||
for i := 0; i < b.N; i++ { | for i := 0; i < b.N; i++ { | ||||
u.HashPassword(pass) | |||||
u.SetPassword(pass) | |||||
} | } | ||||
} | } | ||||
@@ -267,7 +267,10 @@ func EditUserPost(ctx *context.Context, form auth.AdminEditUserForm) { | |||||
ctx.ServerError("UpdateUser", err) | ctx.ServerError("UpdateUser", err) | ||||
return | return | ||||
} | } | ||||
u.HashPassword(form.Password) | |||||
if err = u.SetPassword(form.Password); err != nil { | |||||
ctx.InternalServerError(err) | |||||
return | |||||
} | |||||
} | } | ||||
if len(form.UserName) != 0 && u.Name != form.UserName { | if len(form.UserName) != 0 && u.Name != form.UserName { | ||||
@@ -174,7 +174,10 @@ func EditUser(ctx *context.APIContext, form api.EditUserOption) { | |||||
ctx.Error(http.StatusInternalServerError, "UpdateUser", err) | ctx.Error(http.StatusInternalServerError, "UpdateUser", err) | ||||
return | return | ||||
} | } | ||||
u.HashPassword(form.Password) | |||||
if err = u.SetPassword(form.Password); err != nil { | |||||
ctx.InternalServerError(err) | |||||
return | |||||
} | |||||
} | } | ||||
if form.MustChangePassword != nil { | if form.MustChangePassword != nil { | ||||
@@ -1517,11 +1517,10 @@ func ResetPasswdPost(ctx *context.Context) { | |||||
ctx.ServerError("UpdateUser", err) | ctx.ServerError("UpdateUser", err) | ||||
return | return | ||||
} | } | ||||
if u.Salt, err = models.GetUserSalt(); err != nil { | |||||
if err = u.SetPassword(passwd); err != nil { | |||||
ctx.ServerError("UpdateUser", err) | ctx.ServerError("UpdateUser", err) | ||||
return | return | ||||
} | } | ||||
u.HashPassword(passwd) | |||||
u.MustChangePassword = false | u.MustChangePassword = false | ||||
if err := models.UpdateUserCols(u, "must_change_password", "passwd", "passwd_hash_algo", "rands", "salt"); err != nil { | if err := models.UpdateUserCols(u, "must_change_password", "passwd", "passwd_hash_algo", "rands", "salt"); err != nil { | ||||
ctx.ServerError("UpdateUser", err) | ctx.ServerError("UpdateUser", err) | ||||
@@ -1591,12 +1590,11 @@ func MustChangePasswordPost(ctx *context.Context, cpt *captcha.Captcha, form aut | |||||
} | } | ||||
var err error | var err error | ||||
if u.Salt, err = models.GetUserSalt(); err != nil { | |||||
if err = u.SetPassword(form.Password); err != nil { | |||||
ctx.ServerError("UpdateUser", err) | ctx.ServerError("UpdateUser", err) | ||||
return | return | ||||
} | } | ||||
u.HashPassword(form.Password) | |||||
u.MustChangePassword = false | u.MustChangePassword = false | ||||
if err := models.UpdateUserCols(u, "must_change_password", "passwd", "passwd_hash_algo", "salt"); err != nil { | if err := models.UpdateUserCols(u, "must_change_password", "passwd", "passwd_hash_algo", "salt"); err != nil { | ||||
@@ -63,11 +63,10 @@ func AccountPost(ctx *context.Context, form auth.ChangePasswordForm) { | |||||
ctx.Flash.Error(errMsg) | ctx.Flash.Error(errMsg) | ||||
} else { | } else { | ||||
var err error | var err error | ||||
if ctx.User.Salt, err = models.GetUserSalt(); err != nil { | |||||
if err = ctx.User.SetPassword(form.Password); err != nil { | |||||
ctx.ServerError("UpdateUser", err) | ctx.ServerError("UpdateUser", err) | ||||
return | return | ||||
} | } | ||||
ctx.User.HashPassword(form.Password) | |||||
if err := models.UpdateUserCols(ctx.User, "salt", "passwd_hash_algo", "passwd"); err != nil { | if err := models.UpdateUserCols(ctx.User, "salt", "passwd_hash_algo", "passwd"); err != nil { | ||||
ctx.ServerError("UpdateUser", err) | ctx.ServerError("UpdateUser", err) | ||||
return | return | ||||