summaryrefslogtreecommitdiffstats
path: root/modules/auth/password/hash/hash.go
diff options
context:
space:
mode:
authorDaniel Baumann <daniel@debian.org>2024-10-18 20:33:49 +0200
committerDaniel Baumann <daniel@debian.org>2024-10-18 20:33:49 +0200
commitdd136858f1ea40ad3c94191d647487fa4f31926c (patch)
tree58fec94a7b2a12510c9664b21793f1ed560c6518 /modules/auth/password/hash/hash.go
parentInitial commit. (diff)
downloadforgejo-debian.tar.xz
forgejo-debian.zip
Adding upstream version 9.0.0.HEADupstream/9.0.0upstreamdebian
Signed-off-by: Daniel Baumann <daniel@debian.org>
Diffstat (limited to 'modules/auth/password/hash/hash.go')
-rw-r--r--modules/auth/password/hash/hash.go189
1 files changed, 189 insertions, 0 deletions
diff --git a/modules/auth/password/hash/hash.go b/modules/auth/password/hash/hash.go
new file mode 100644
index 0000000..459320e
--- /dev/null
+++ b/modules/auth/password/hash/hash.go
@@ -0,0 +1,189 @@
+// 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
+}