mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-11-15 14:36:18 +01:00
c702e7995d
Backport #22942 This PR refactors and improves the password hashing code within gitea and makes it possible for server administrators to set the password hashing parameters In addition it takes the opportunity to adjust the settings for `pbkdf2` in order to make the hashing a little stronger. The majority of this work was inspired by PR #14751 and I would like to thank @boppy for their work on this. Thanks to @gusted for the suggestion to adjust the `pbkdf2` hashing parameters. Close #14751 --------- Signed-off-by: Andrew Thornton <art27@cantab.net> Co-authored-by: delvh <dev.lh@web.de> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
147 lines
4.2 KiB
Go
147 lines
4.2 KiB
Go
// Copyright 2023 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package hash
|
|
|
|
import (
|
|
"crypto/subtle"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"strings"
|
|
"sync/atomic"
|
|
|
|
"code.gitea.io/gitea/modules/log"
|
|
)
|
|
|
|
// This package takes care of hashing passwords, verifying passwords, defining
|
|
// available password algorithms, defining recommended password algorithms and
|
|
// choosing the default password algorithm.
|
|
|
|
// PasswordSaltHasher will hash a provided password with the provided saltBytes
|
|
type PasswordSaltHasher interface {
|
|
HashWithSaltBytes(password string, saltBytes []byte) string
|
|
}
|
|
|
|
// PasswordHasher will hash a provided password with the salt
|
|
type PasswordHasher interface {
|
|
Hash(password, salt string) (string, error)
|
|
}
|
|
|
|
// PasswordVerifier will ensure that a providedPassword matches the hashPassword when hashed with the salt
|
|
type PasswordVerifier interface {
|
|
VerifyPassword(providedPassword, hashedPassword, salt string) bool
|
|
}
|
|
|
|
// PasswordHashAlgorithms are named PasswordSaltHashers with a default verifier and hash function
|
|
type PasswordHashAlgorithm struct {
|
|
PasswordSaltHasher
|
|
Name string
|
|
}
|
|
|
|
// Hash the provided password with the salt and return the hash
|
|
func (algorithm *PasswordHashAlgorithm) Hash(password, salt string) (string, error) {
|
|
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.
|
|
if len(salt) == 10 {
|
|
saltBytes = []byte(salt)
|
|
} else {
|
|
var err error
|
|
saltBytes, err = hex.DecodeString(salt)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
|
|
return algorithm.HashWithSaltBytes(password, saltBytes), nil
|
|
}
|
|
|
|
// Verify the provided password matches the hashPassword when hashed with the salt
|
|
func (algorithm *PasswordHashAlgorithm) VerifyPassword(providedPassword, hashedPassword, salt string) bool {
|
|
// The bcrypt package has its own specialized compare function that takes into
|
|
// account the stored password's bcrypt parameters.
|
|
if verifier, ok := algorithm.PasswordSaltHasher.(PasswordVerifier); ok {
|
|
return verifier.VerifyPassword(providedPassword, hashedPassword, salt)
|
|
}
|
|
|
|
// Compute the hash of the password.
|
|
providedPasswordHash, err := algorithm.Hash(providedPassword, salt)
|
|
if err != nil {
|
|
log.Error("passwordhash: %v.Hash(): %v", algorithm.Name, err)
|
|
return false
|
|
}
|
|
|
|
// Compare it against the hashed password in constant-time.
|
|
return subtle.ConstantTimeCompare([]byte(hashedPassword), []byte(providedPasswordHash)) == 1
|
|
}
|
|
|
|
var (
|
|
lastNonDefaultAlgorithm atomic.Value
|
|
availableHasherFactories = map[string]func(string) PasswordSaltHasher{}
|
|
)
|
|
|
|
// Register registers a PasswordSaltHasher with the availableHasherFactories
|
|
// This is not thread safe.
|
|
func Register[T PasswordSaltHasher](name string, newFn func(config string) T) {
|
|
if _, has := availableHasherFactories[name]; has {
|
|
panic(fmt.Errorf("duplicate registration of password salt hasher: %s", name))
|
|
}
|
|
|
|
availableHasherFactories[name] = func(config string) PasswordSaltHasher {
|
|
n := newFn(config)
|
|
return n
|
|
}
|
|
}
|
|
|
|
// In early versions of gitea the password hash algorithm field could be empty
|
|
// At that point the default was `pbkdf2` without configuration values
|
|
// Please note this is not the same as the DefaultAlgorithm
|
|
const defaultEmptyHashAlgorithmName = "pbkdf2"
|
|
|
|
func Parse(algorithm string) *PasswordHashAlgorithm {
|
|
if algorithm == "" {
|
|
algorithm = defaultEmptyHashAlgorithmName
|
|
}
|
|
|
|
if DefaultHashAlgorithm != nil && algorithm == DefaultHashAlgorithm.Name {
|
|
return DefaultHashAlgorithm
|
|
}
|
|
|
|
ptr := lastNonDefaultAlgorithm.Load()
|
|
if ptr != nil {
|
|
hashAlgorithm, ok := ptr.(*PasswordHashAlgorithm)
|
|
if ok && hashAlgorithm.Name == algorithm {
|
|
return hashAlgorithm
|
|
}
|
|
}
|
|
|
|
vals := strings.SplitN(algorithm, "$", 2)
|
|
var name string
|
|
var config string
|
|
if len(vals) == 0 {
|
|
return nil
|
|
}
|
|
name = vals[0]
|
|
if len(vals) > 1 {
|
|
config = vals[1]
|
|
}
|
|
newFn, has := availableHasherFactories[name]
|
|
if !has {
|
|
return nil
|
|
}
|
|
ph := newFn(config)
|
|
if ph == nil {
|
|
return nil
|
|
}
|
|
hashAlgorithm := &PasswordHashAlgorithm{
|
|
PasswordSaltHasher: ph,
|
|
Name: algorithm,
|
|
}
|
|
|
|
lastNonDefaultAlgorithm.Store(hashAlgorithm)
|
|
|
|
return hashAlgorithm
|
|
}
|