mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 11:35:03 +01: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>
		
			
				
	
	
		
			190 lines
		
	
	
		
			6.3 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			190 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
 | |
| }
 |