mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-08-21 20:18:42 +02:00
During the recent hash algorithm change it became clear that the choice of password hash algorithm plays a role in the time taken for CI to run. Therefore as attempt to improve CI we should consider using a dummy hashing algorithm instead of a real hashing algorithm. This PR creates a dummy algorithm which is then set as the default hashing algorithm during tests that use the fixtures. This hopefully will cause a reduction in the time it takes for CI to run. --------- Signed-off-by: Andrew Thornton <art27@cantab.net> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
189 lines
6.3 KiB
Go
189 lines
6.3 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{}
|
|
)
|
|
|
|
// MustRegister registers a PasswordSaltHasher with the availableHasherFactories
|
|
// Caution: This is not thread safe.
|
|
func MustRegister[T PasswordSaltHasher](name string, newFn func(config string) T) {
|
|
if err := Register(name, newFn); err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
// Register registers a PasswordSaltHasher with the availableHasherFactories
|
|
// Caution: This is not thread safe.
|
|
func Register[T PasswordSaltHasher](name string, newFn func(config string) T) error {
|
|
if _, has := availableHasherFactories[name]; has {
|
|
return fmt.Errorf("duplicate registration of password salt hasher: %s", name)
|
|
}
|
|
|
|
availableHasherFactories[name] = func(config string) PasswordSaltHasher {
|
|
n := newFn(config)
|
|
return n
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// 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
|
|
}
|