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

Increase Salt randomness #18179

Merged
merged 8 commits into from
Jan 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
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 models/migrations/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,8 @@ var migrations = []Migration{
NewMigration("Add Sorting to ProjectIssue table", addProjectIssueSorting),
// v204 -> v205
NewMigration("Add key is verified to ssh key", addSSHKeyIsVerified),
// v205 -> v206
NewMigration("Migrate to higher varchar on user struct", migrateUserPasswordSalt),
}

// GetCurrentDBVersion returns the current db version
Expand Down
39 changes: 39 additions & 0 deletions models/migrations/v205.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright 2022 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 (
"xorm.io/xorm"
"xorm.io/xorm/schemas"
)

func migrateUserPasswordSalt(x *xorm.Engine) error {
dbType := x.Dialect().URI().DBType
// For SQLITE, the max length doesn't matter.
if dbType == schemas.SQLITE {
return nil
}

if err := modifyColumn(x, "user", &schemas.Column{
Name: "rands",
SQLType: schemas.SQLType{
Name: "VARCHAR",
},
Length: 32,
// MySQL will like us again.
Nullable: true,
}); err != nil {
return err
}

return modifyColumn(x, "user", &schemas.Column{
Name: "salt",
SQLType: schemas.SQLType{
Name: "VARCHAR",
},
Length: 32,
Nullable: true,
})
}
53 changes: 42 additions & 11 deletions models/user/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@ type User struct {
Type UserType
Location string
Website string
Rands string `xorm:"VARCHAR(10)"`
Salt string `xorm:"VARCHAR(10)"`
Rands string `xorm:"VARCHAR(32)"`
Salt string `xorm:"VARCHAR(32)"`
Language string `xorm:"VARCHAR(5)"`
Description string

Expand Down Expand Up @@ -358,24 +358,40 @@ func (u *User) NewGitSig() *git.Signature {
}
}

func hashPassword(passwd, salt, algo string) string {
func hashPassword(passwd, salt, algo string) (string, error) {
var tempPasswd []byte
var saltBytes []byte

// There are two formats for the Salt value:
// * The new format is a (32+)-byte hex-encoded string
// * The old format was a 10-byte binary format
// We have to tolerate both here but Authenticate should
// regenerate the Salt following a successful validation.
if len(salt) == 10 {
saltBytes = []byte(salt)
} else {
var err error
saltBytes, err = hex.DecodeString(salt)
if err != nil {
return "", err
}
}

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

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

// SetPassword hashes a password using the algorithm defined in the config value of PASSWORD_HASH_ALGO
Expand All @@ -391,15 +407,20 @@ func (u *User) SetPassword(passwd string) (err error) {
if u.Salt, err = GetUserSalt(); err != nil {
return err
}
if u.Passwd, err = hashPassword(passwd, u.Salt, setting.PasswordHashAlgo); err != nil {
return err
}
u.PasswdHashAlgo = setting.PasswordHashAlgo
u.Passwd = hashPassword(passwd, u.Salt, setting.PasswordHashAlgo)

return nil
}

// ValidatePassword checks if given password matches the one belongs to the user.
func (u *User) ValidatePassword(passwd string) bool {
tempHash := hashPassword(passwd, u.Salt, u.PasswdHashAlgo)
tempHash, err := hashPassword(passwd, u.Salt, u.PasswdHashAlgo)
if err != nil {
return false
}

if u.PasswdHashAlgo != algoBcrypt && subtle.ConstantTimeCompare([]byte(u.Passwd), []byte(tempHash)) == 1 {
return true
Expand Down Expand Up @@ -505,9 +526,19 @@ func IsUserExist(uid int64, name string) (bool, error) {
return isUserExist(db.GetEngine(db.DefaultContext), uid, name)
}

// Note: As of the beginning of 2022, it is recommended to use at least
// 64 bits of salt, but NIST is already recommending to use to 128 bits.
// (16 bytes = 16 * 8 = 128 bits)
const SaltByteLength = 16

// GetUserSalt returns a random user salt token.
func GetUserSalt() (string, error) {
return util.RandomString(10)
rBytes, err := util.RandomBytes(SaltByteLength)
if err != nil {
return "", err
}
// Returns a 32 bytes long string.
return hex.EncodeToString(rBytes), nil
}

// NewGhostUser creates and returns a fake user for someone has deleted his/her account.
Expand Down
13 changes: 11 additions & 2 deletions modules/util/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,11 +139,11 @@ func MergeInto(dict map[string]interface{}, values ...interface{}) (map[string]i

// RandomInt returns a random integer between 0 and limit, inclusive
func RandomInt(limit int64) (int64, error) {
int, err := rand.Int(rand.Reader, big.NewInt(limit))
rInt, err := rand.Int(rand.Reader, big.NewInt(limit))
if err != nil {
return 0, err
}
return int.Int64(), nil
return rInt.Int64(), nil
}

const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
Expand All @@ -161,3 +161,12 @@ func RandomString(length int64) (string, error) {
}
return string(bytes), nil
}

// RandomBytes generates `length` bytes
// This differs from RandomString, as RandomString is limits each byte to have
// a maximum value of 63 instead of 255(max byte size)
func RandomBytes(length int64) ([]byte, error) {
bytes := make([]byte, length)
_, err := rand.Read(bytes)
return bytes, err
}
18 changes: 18 additions & 0 deletions modules/util/util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,24 @@ func Test_RandomString(t *testing.T) {
assert.NotEqual(t, str3, str4)
}

func Test_RandomBytes(t *testing.T) {
bytes1, err := RandomBytes(32)
assert.NoError(t, err)

bytes2, err := RandomBytes(32)
assert.NoError(t, err)

assert.NotEqual(t, bytes1, bytes2)

bytes3, err := RandomBytes(256)
assert.NoError(t, err)

bytes4, err := RandomBytes(256)
assert.NoError(t, err)

assert.NotEqual(t, bytes3, bytes4)
}

func Test_OptionalBool(t *testing.T) {
assert.Equal(t, OptionalBoolNone, OptionalBoolParse(""))
assert.Equal(t, OptionalBoolNone, OptionalBoolParse("x"))
Expand Down
4 changes: 3 additions & 1 deletion services/auth/source/db/authenticate.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ func Authenticate(user *user_model.User, login, password string) (*user_model.Us
}

// Update password hash if server password hash algorithm have changed
if user.PasswdHashAlgo != setting.PasswordHashAlgo {
// Or update the password when the salt length doesn't match the current
// recommended salt length, this in order to migrate user's salts to a more secure salt.
if user.PasswdHashAlgo != setting.PasswordHashAlgo || len(user.Salt) != user_model.SaltByteLength*2 {
if err := user.SetPassword(password); err != nil {
return nil, err
}
Expand Down