61b89747ed
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: John Olheiser <john.olheiser@gmail.com> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
180 lines
6.1 KiB
Go
180 lines
6.1 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
|
|
Specification string // The specification that is used to create the internal PasswordSaltHasher
|
|
}
|
|
|
|
// 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 {
|
|
// Some PasswordSaltHashers have their own specialised compare function that takes into
|
|
// account the stored parameters within the hash. e.g. bcrypt
|
|
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.Specification, 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
|
|
// Caution: 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 of a user could be
|
|
// empty. At that point the default was `pbkdf2` without configuration values
|
|
//
|
|
// Please note this is not the same as the DefaultAlgorithm which is used
|
|
// to determine what an empty PASSWORD_HASH_ALGO setting in the app.ini means.
|
|
// These are not the same even if they have the same apparent value and they mean different things.
|
|
//
|
|
// DO NOT COALESCE THESE VALUES
|
|
const defaultEmptyHashAlgorithmSpecification = "pbkdf2"
|
|
|
|
// Parse will convert the provided algorithm specification in to a PasswordHashAlgorithm
|
|
// If the provided specification matches the DefaultHashAlgorithm Specification it will be
|
|
// used.
|
|
// In addition the last non-default hasher will be cached to help reduce the load from
|
|
// parsing specifications.
|
|
//
|
|
// NOTE: No de-aliasing is done in this function, thus any specification which does not
|
|
// contain a configuration will use the default values for that hasher. These are not
|
|
// necessarily the same values as those obtained by dealiasing. This allows for
|
|
// seamless backwards compatibility with the original configuration.
|
|
//
|
|
// To further labour this point, running `Parse("pbkdf2")` does not obtain the
|
|
// same algorithm as setting `PASSWORD_HASH_ALGO=pbkdf2` in app.ini, nor is it intended to.
|
|
// A user that has `password_hash_algo='pbkdf2'` in the db means get the original, unconfigured algorithm
|
|
// Users will be migrated automatically as they log-in to have the complete specification stored
|
|
// in their `password_hash_algo` fields by other code.
|
|
func Parse(algorithmSpec string) *PasswordHashAlgorithm {
|
|
if algorithmSpec == "" {
|
|
algorithmSpec = defaultEmptyHashAlgorithmSpecification
|
|
}
|
|
|
|
if DefaultHashAlgorithm != nil && algorithmSpec == DefaultHashAlgorithm.Specification {
|
|
return DefaultHashAlgorithm
|
|
}
|
|
|
|
ptr := lastNonDefaultAlgorithm.Load()
|
|
if ptr != nil {
|
|
hashAlgorithm, ok := ptr.(*PasswordHashAlgorithm)
|
|
if ok && hashAlgorithm.Specification == algorithmSpec {
|
|
return hashAlgorithm
|
|
}
|
|
}
|
|
|
|
// Now convert the provided specification in to a hasherType +/- some configuration parameters
|
|
vals := strings.SplitN(algorithmSpec, "$", 2)
|
|
var hasherType string
|
|
var config string
|
|
|
|
if len(vals) == 0 {
|
|
// This should not happen as algorithmSpec should not be empty
|
|
// due to it being assigned to defaultEmptyHashAlgorithmSpecification above
|
|
// but we should be absolutely cautious here
|
|
return nil
|
|
}
|
|
|
|
hasherType = vals[0]
|
|
if len(vals) > 1 {
|
|
config = vals[1]
|
|
}
|
|
|
|
newFn, has := availableHasherFactories[hasherType]
|
|
if !has {
|
|
// unknown hasher type
|
|
return nil
|
|
}
|
|
|
|
ph := newFn(config)
|
|
if ph == nil {
|
|
// The provided configuration is likely invalid - it will have been logged already
|
|
// but we cannot hash safely
|
|
return nil
|
|
}
|
|
|
|
hashAlgorithm := &PasswordHashAlgorithm{
|
|
PasswordSaltHasher: ph,
|
|
Specification: algorithmSpec,
|
|
}
|
|
|
|
lastNonDefaultAlgorithm.Store(hashAlgorithm)
|
|
|
|
return hashAlgorithm
|
|
}
|