Skip to content

Commit

Permalink
Introduce ability to update HashAlgo
Browse files Browse the repository at this point in the history
- Add Defaults in settings
- Add Flag to Update Password on next use in settings
- Create Crypto-Fallbacks in structs
- Update Hashing Generation
  • Loading branch information
boppy committed Feb 19, 2021
1 parent b3c2e23 commit b2728cf
Show file tree
Hide file tree
Showing 5 changed files with 331 additions and 22 deletions.
18 changes: 17 additions & 1 deletion models/fixtures/user.yml
Original file line number Diff line number Diff line change
Expand Up @@ -508,7 +508,6 @@
num_repos: 0
is_active: true


-
id: 30
lower_name: user30
Expand All @@ -525,3 +524,20 @@
avatar_email: user30@example.com
num_repos: 2
is_active: true

-
id: 31
lower_name: user31
name: user31
full_name: User ThirtyOne
email: user31@example.com
passwd_hash_algo: argon2
passwd: "$argon2$65536$2$8$50$a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b"
type: 0 # individual
salt: ZogKvWdyEx
is_admin: false
is_restricted: true
avatar: avatar29
avatar_email: user31@example.com
num_repos: 0
is_active: true
174 changes: 155 additions & 19 deletions models/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
"unicode/utf8"
Expand Down Expand Up @@ -52,10 +53,13 @@ const (
)

const (
algoBcrypt = "bcrypt"
algoScrypt = "scrypt"
algoArgon2 = "argon2"
algoPbkdf2 = "pbkdf2"
algoBcrypt = "bcrypt"
algoScrypt = "scrypt"
formatScrypt = "$%s$%d$%d$%d$%d$%x"
algoArgon2 = "argon2"
formatArgon2 = "$%s$%d$%d$%d$%d$%x"
algoPbkdf2 = "pbkdf2"
formatPbkdf2 = "$%s$%d$%d$%x"

// EmailNotificationsEnabled indicates that the user would like to receive all email notifications
EmailNotificationsEnabled = "enabled"
Expand Down Expand Up @@ -374,24 +378,137 @@ func (u *User) NewGitSig() *git.Signature {
}
}

func hashPassword(passwd, salt, algo string) string {
func hashPasswordBcrypt(passwd string, params structs.CryptBCrypt) (string, string) {
var tempPasswd []byte
tempPasswd, _ = bcrypt.GenerateFromPassword([]byte(passwd), params.Cost)
return string(tempPasswd),
string(tempPasswd)
}
func hashPasswordScrypt(passwd string, salt string, params structs.CryptSCrypt) (string, string) {
var tempPasswd []byte
tempPasswd, _ = scrypt.Key([]byte(passwd), []byte(salt), params.N, params.R, params.P, params.KeyLength)
return fmt.Sprintf(formatScrypt, algoScrypt, params.N, params.R, params.P, params.KeyLength, tempPasswd),
fmt.Sprintf("%x", tempPasswd)
}
func hashPasswordArgon2(passwd string, salt string, params structs.CryptArgon2) (string, string) {
var tempPasswd = argon2.IDKey([]byte(passwd), []byte(salt), params.Iterations, params.Memory, params.Parallelism, params.KeyLength)
return fmt.Sprintf(formatArgon2, algoArgon2, params.Memory, params.Iterations, params.Parallelism, params.KeyLength, tempPasswd),
fmt.Sprintf("%x", tempPasswd)
}
func hashPasswordPbkdf2(passwd string, salt string, params structs.CryptPbkdf2) (string, string) {
var tempPasswd = pbkdf2.Key([]byte(passwd), []byte(salt), params.Iterations, params.KeyLength, sha256.New)
return fmt.Sprintf(formatPbkdf2, algoPbkdf2, params.Iterations, params.KeyLength, tempPasswd),
fmt.Sprintf("%x", tempPasswd)
}

func hashPasswordWithConfig(passwd, salt, algo string) (string, string) {
switch algo {
case algoBcrypt:
tempPasswd, _ = bcrypt.GenerateFromPassword([]byte(passwd), bcrypt.DefaultCost)
return string(tempPasswd)
return hashPasswordBcrypt(passwd, setting.BCryptParams)
case algoScrypt:
tempPasswd, _ = scrypt.Key([]byte(passwd), []byte(salt), 65536, 16, 2, 50)
return hashPasswordScrypt(passwd, salt, setting.SCryptParams)
case algoArgon2:
tempPasswd = argon2.IDKey([]byte(passwd), []byte(salt), 2, 65536, 8, 50)
return hashPasswordArgon2(passwd, salt, setting.Argon2Params)
case algoPbkdf2:
fallthrough
default:
tempPasswd = pbkdf2.Key([]byte(passwd), []byte(salt), 10000, 50, sha256.New)
return hashPasswordPbkdf2(passwd, salt, setting.Pbkdf2Params)
}
}
func hashPasswordWithFallbackDefaults(passwd, salt, algo string) (string, string) {
switch algo {
case algoBcrypt:
return hashPasswordBcrypt(passwd, structs.BCryptFallback)
case algoScrypt:
return hashPasswordScrypt(passwd, salt, structs.SCryptFallback)
case algoArgon2:
return hashPasswordArgon2(passwd, salt, structs.Argon2Fallback)
case algoPbkdf2:
fallthrough
default:
return hashPasswordPbkdf2(passwd, salt, structs.Pbkdf2Fallback)
}
}

func hashPasswordWithCurrentDefaults(passwd string, u *User) (string, string, bool) {
currentPasswd := u.Passwd
salt := u.Salt
algo := u.PasswdHashAlgo
split := strings.Split(currentPasswd, "$")

var hash string
var matchesActiveSettings bool

switch algo {
case algoBcrypt:
cost, _ := strconv.Atoi(split[2])

params := structs.CryptBCrypt{
Cost: cost,
}
matchesActiveSettings = u.PasswdHashAlgo == algo && setting.BCryptParams == params
hash, _ = hashPasswordBcrypt(passwd, params)

case algoScrypt:
var n, r, p, keyLength int

n, _ = strconv.Atoi(split[2])
r, _ = strconv.Atoi(split[3])
p, _ = strconv.Atoi(split[4])
keyLength, _ = strconv.Atoi(split[5])

params := structs.CryptSCrypt{
N: n,
R: r,
P: p,
KeyLength: keyLength,
}
matchesActiveSettings = u.PasswdHashAlgo == algo && setting.SCryptParams == params
hash, _ = hashPasswordScrypt(passwd, salt, params)

return fmt.Sprintf("%x", tempPasswd)
case algoArgon2:
var iterations, memory, keyLength uint32
var parallelism uint8
var tempInt int

tempInt, _ = strconv.Atoi(split[2])
memory = uint32(tempInt)

tempInt, _ = strconv.Atoi(split[3])
iterations = uint32(tempInt)

tempInt, _ = strconv.Atoi(split[4])
parallelism = uint8(tempInt)

tempInt, _ = strconv.Atoi(split[5])
keyLength = uint32(tempInt)

params := structs.CryptArgon2{
Iterations: iterations,
Memory: memory,
Parallelism: parallelism,
KeyLength: keyLength,
}
matchesActiveSettings = u.PasswdHashAlgo == algo && setting.Argon2Params == params
hash, _ = hashPasswordArgon2(passwd, salt, params)

case algoPbkdf2:
fallthrough
default:
var iterations, keyLength int

iterations, _ = strconv.Atoi(split[2])
keyLength, _ = strconv.Atoi(split[3])

params := structs.CryptPbkdf2{
Iterations: iterations,
KeyLength: keyLength,
}
matchesActiveSettings = u.PasswdHashAlgo == algo && setting.Pbkdf2Params == params
hash, _ = hashPasswordPbkdf2(passwd, salt, params)
}

return hash, algo, matchesActiveSettings
}

// SetPassword hashes a password using the algorithm defined in the config value of PASSWORD_HASH_ALGO
Expand All @@ -407,23 +524,42 @@ func (u *User) SetPassword(passwd string) (err error) {
if u.Salt, err = GetUserSalt(); err != nil {
return err
}

// Force algo Update
u.PasswdHashAlgo = setting.PasswordHashAlgo
u.Passwd = hashPassword(passwd, u.Salt, setting.PasswordHashAlgo)
u.Passwd, _ = hashPasswordWithConfig(passwd, u.Salt, u.PasswdHashAlgo)

return nil
}

// ValidatePassword checks if given password matches the one belongs to the user.
// ValidatePassword checks if given password matches the one belonging to the user.
func (u *User) ValidatePassword(passwd string) bool {
tempHash := hashPassword(passwd, u.Salt, u.PasswdHashAlgo)
var tempHash, algo string
var matchesSettings bool
if u.Passwd[:1] == "$" {
// Hash with known settings
tempHash, algo, matchesSettings = hashPasswordWithCurrentDefaults(passwd, u)
} else {
// Hash with defaults
algo = u.PasswdHashAlgo
matchesSettings = false // always false, because new password format should be enforced
_, tempHash = hashPasswordWithFallbackDefaults(passwd, u.Salt, u.PasswdHashAlgo)
}

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

if matches && setting.PasswordUpdateAlgoToCurrent && (!matchesSettings || algo != setting.PasswordHashAlgo) {
// @todo: need to handle this error? As far as I can see, passwd is always set at this point...
_ = u.SetPassword(passwd)
}

return matches
}

// IsPasswordSet checks if the password is set or left empty
Expand Down
64 changes: 62 additions & 2 deletions models/user_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,13 +136,13 @@ func TestSearchUsers(t *testing.T) {
}

testUserSuccess(&SearchUserOptions{OrderBy: "id ASC", ListOptions: ListOptions{Page: 1}},
[]int64{1, 2, 4, 5, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21, 24, 27, 28, 29, 30})
[]int64{1, 2, 4, 5, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21, 24, 27, 28, 29, 30, 31})

testUserSuccess(&SearchUserOptions{ListOptions: ListOptions{Page: 1}, IsActive: util.OptionalBoolFalse},
[]int64{9})

testUserSuccess(&SearchUserOptions{OrderBy: "id ASC", ListOptions: ListOptions{Page: 1}, IsActive: util.OptionalBoolTrue},
[]int64{1, 2, 4, 5, 8, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21, 24, 28, 29, 30})
[]int64{1, 2, 4, 5, 8, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21, 24, 28, 29, 30, 31})

testUserSuccess(&SearchUserOptions{Keyword: "user1", OrderBy: "id ASC", ListOptions: ListOptions{Page: 1}, IsActive: util.OptionalBoolTrue},
[]int64{1, 10, 11, 12, 13, 14, 15, 16, 18})
Expand Down Expand Up @@ -243,6 +243,66 @@ func TestHashPasswordDeterministic(t *testing.T) {
}
}

func TestOldPasswordMatchAndUpdate(t *testing.T) {
assert.NoError(t, PrepareTestDatabase())
u := AssertExistsAndLoadBean(t, &User{ID: 1}).(*User)

setting.PasswordHashAlgo = algoArgon2

matchingPass := "password"
oldPass := u.Passwd

// Without update function
setting.PasswordUpdateAlgoToCurrent = false
validates := u.ValidatePassword(matchingPass)
newPass := u.Passwd
// Should match with old algo
assert.True(t, validates)
// Should not be updated to new format
assert.Equal(t, oldPass, newPass)

// With update function
setting.PasswordUpdateAlgoToCurrent = true
validates = u.ValidatePassword(matchingPass)
newPass = u.Passwd

// Should match with old algo
assert.True(t, validates)
// Should be updated to new format
assert.NotEqual(t, oldPass, newPass)
}

func TestNewPasswordMatch(t *testing.T) {
assert.NoError(t, PrepareTestDatabase())
u := AssertExistsAndLoadBean(t, &User{ID: 31}).(*User)

setting.PasswordHashAlgo = algoArgon2

matchingPass := "password"
oldPass := u.Passwd

// Without update function
setting.PasswordUpdateAlgoToCurrent = false
validates := u.ValidatePassword(matchingPass)
newPass := u.Passwd

// Should match
assert.True(t, validates)
// Should not be updated
assert.Equal(t, oldPass, newPass)

// With update function and different default HashAlgo
setting.PasswordUpdateAlgoToCurrent = true
setting.PasswordHashAlgo = algoBcrypt
passwordMatches := u.ValidatePassword(matchingPass)
newPass = u.Passwd

// Should match
assert.True(t, passwordMatches)
// Should not be updated
assert.NotEqual(t, oldPass, newPass)
}

func BenchmarkHashPassword(b *testing.B) {
// BenchmarkHashPassword ensures that it takes a reasonable amount of time
// to hash a password - in order to protect from brute-force attacks.
Expand Down
30 changes: 30 additions & 0 deletions modules/setting/setting.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,13 @@ import (

"code.gitea.io/gitea/modules/generate"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/user"
"code.gitea.io/gitea/modules/util"

shellquote "github.com/kballard/go-shellquote"
"github.com/unknwon/com"
"golang.org/x/crypto/bcrypt"
gossh "golang.org/x/crypto/ssh"
ini "gopkg.in/ini.v1"
)
Expand Down Expand Up @@ -161,6 +163,34 @@ var (
PasswordComplexity []string
PasswordHashAlgo string
PasswordCheckPwn bool
PasswordUpdateAlgoToCurrent bool

// BCryptParams stores parameters for bcrypt algo
BCryptParams = structs.CryptBCrypt{
Cost: bcrypt.DefaultCost,
}

// SCryptParams stores parameters for scrypt algo
SCryptParams = structs.CryptSCrypt{
N: 65536,
R: 16,
P: 2,
KeyLength: 50,
}

// Argon2Params stores params for argon2 algo
Argon2Params = structs.CryptArgon2{
Iterations: 2,
Memory: 65536,
Parallelism: 8,
KeyLength: 50,
}

// Pbkdf2Params stores parameters for pbkdf2 algo
Pbkdf2Params = structs.CryptPbkdf2{
Iterations: 10000,
KeyLength: 50,
}

// UI settings
UI = struct {
Expand Down
Loading

0 comments on commit b2728cf

Please sign in to comment.