forked from NYANDEV/forgejo
[Refactor] Passwort Hash/Set (#14282)
* 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 stuff
This commit is contained in:
parent
6b3b6f1833
commit
74a0481586
10 changed files with 158 additions and 32 deletions
|
@ -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 = user.SetPassword(password); err != nil {
|
||||||
if err := UpdateUserCols(user, "passwd", "passwd_hash_algo"); 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
|
||||||
|
|
115
models/migrations/v166.go
Normal file
115
models/migrations/v166.go
Normal file
|
@ -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.
|
// SetPassword hashes a password using the algorithm defined in the config value of PASSWORD_HASH_ALGO
|
||||||
func (u *User) HashPassword(passwd string) {
|
// 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{}
|
||||||
u := &User{Salt: string(b)}
|
|
||||||
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
|
assert.NotEqual(t, r1, r2)
|
||||||
if u.PasswdHashAlgo == "bcrypt" {
|
assert.True(t, u.ValidatePassword(pass))
|
||||||
assert.NotEqual(t, r1, r2)
|
|
||||||
} else {
|
|
||||||
assert.Equal(t, r1, r2)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
u := &User{Passwd: pass}
|
||||||
rand.Read(bs)
|
|
||||||
u := &User{Salt: string(bs), 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
|
||||||
|
|
Loading…
Reference in a new issue