// 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 }