From 7cad01fec846d857bc79cbd90ac9e3a45bf261b1 Mon Sep 17 00:00:00 2001
From: Andrew Thornton <art27@cantab.net>
Date: Thu, 2 Feb 2023 13:53:19 +0100
Subject: [PATCH 01/10] Provide the ability to set password hash algorithm
 parameters

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>
---
 cmd/admin_user_change_password.go             |   2 +-
 cmd/admin_user_create.go                      |   2 +-
 .../doc/advanced/config-cheat-sheet.en-us.md  |  16 +-
 models/user/user.go                           |  75 +------
 models/user/user_test.go                      |   3 +-
 modules/auth/password/hash/argon2.go          |  76 +++++++
 modules/auth/password/hash/bcrypt.go          |  51 +++++
 modules/auth/password/hash/common.go          |  28 +++
 modules/auth/password/hash/hash.go            | 147 ++++++++++++++
 modules/auth/password/hash/hash_test.go       | 186 ++++++++++++++++++
 modules/auth/password/hash/pbkdf2.go          |  61 ++++++
 modules/auth/password/hash/scrypt.go          |  63 ++++++
 modules/auth/password/hash/setting.go         |  40 ++++
 modules/auth/password/hash/setting_test.go    |  38 ++++
 modules/{ => auth}/password/password.go       |   8 +-
 modules/{ => auth}/password/password_test.go  |   0
 modules/{ => auth}/password/pwn.go            |   2 +-
 modules/{ => auth}/password/pwn/pwn.go        |   0
 modules/{ => auth}/password/pwn/pwn_test.go   |   0
 modules/setting/setting.go                    |  10 +-
 routers/api/v1/admin/user.go                  |   2 +-
 routers/install/install.go                    |   3 +-
 routers/web/admin/users.go                    |   2 +-
 routers/web/auth/auth.go                      |   2 +-
 routers/web/auth/password.go                  |   2 +-
 routers/web/user/setting/account.go           |   2 +-
 26 files changed, 734 insertions(+), 87 deletions(-)
 create mode 100644 modules/auth/password/hash/argon2.go
 create mode 100644 modules/auth/password/hash/bcrypt.go
 create mode 100644 modules/auth/password/hash/common.go
 create mode 100644 modules/auth/password/hash/hash.go
 create mode 100644 modules/auth/password/hash/hash_test.go
 create mode 100644 modules/auth/password/hash/pbkdf2.go
 create mode 100644 modules/auth/password/hash/scrypt.go
 create mode 100644 modules/auth/password/hash/setting.go
 create mode 100644 modules/auth/password/hash/setting_test.go
 rename modules/{ => auth}/password/password.go (93%)
 rename modules/{ => auth}/password/password_test.go (100%)
 rename modules/{ => auth}/password/pwn.go (93%)
 rename modules/{ => auth}/password/pwn/pwn.go (100%)
 rename modules/{ => auth}/password/pwn/pwn_test.go (100%)

diff --git a/cmd/admin_user_change_password.go b/cmd/admin_user_change_password.go
index 1b7c73370d433..7866bde9128b9 100644
--- a/cmd/admin_user_change_password.go
+++ b/cmd/admin_user_change_password.go
@@ -9,7 +9,7 @@ import (
 	"fmt"
 
 	user_model "code.gitea.io/gitea/models/user"
-	pwd "code.gitea.io/gitea/modules/password"
+	pwd "code.gitea.io/gitea/modules/auth/password"
 	"code.gitea.io/gitea/modules/setting"
 
 	"github.com/urfave/cli"
diff --git a/cmd/admin_user_create.go b/cmd/admin_user_create.go
index 579c6f2f62bb6..09eaad54becf9 100644
--- a/cmd/admin_user_create.go
+++ b/cmd/admin_user_create.go
@@ -10,7 +10,7 @@ import (
 
 	auth_model "code.gitea.io/gitea/models/auth"
 	user_model "code.gitea.io/gitea/models/user"
-	pwd "code.gitea.io/gitea/modules/password"
+	pwd "code.gitea.io/gitea/modules/auth/password"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 
diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md
index 04344b15dc73f..150acb7ecbf60 100644
--- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md
+++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md
@@ -568,7 +568,21 @@ Certain queues have defaults that override the defaults set in `[queue]` (this o
 - `IMPORT_LOCAL_PATHS`: **false**: Set to `false` to prevent all users (including admin) from importing local path on server.
 - `INTERNAL_TOKEN`: **\<random at every install if no uri set\>**: Secret used to validate communication within Gitea binary.
 - `INTERNAL_TOKEN_URI`: **<empty>**: Instead of defining INTERNAL_TOKEN in the configuration, this configuration option can be used to give Gitea a path to a file that contains the internal token (example value: `file:/etc/gitea/internal_token`)
-- `PASSWORD_HASH_ALGO`: **pbkdf2**: The hash algorithm to use \[argon2, pbkdf2, scrypt, bcrypt\], argon2 will spend more memory than others.
+- `PASSWORD_HASH_ALGO`: **pbkdf2**: The hash algorithm to use \[argon2, pbkdf2, pbkdf2_v1, scrypt, bcrypt\], argon2 and scrypt will spend significant amounts of memory.
+  - Note: The default parameters for `pbkdf2` hashing have changed - the previous settings are available as `pbkdf2_v1` but are not recommended.
+  - The hash functions may be tuned by using `$` after the algorithm:
+    - `argon2$<time>$<memory>$<threads>$<key-length>`
+    - `pbkdf2$<iterations>$<key-length>`
+    - `scrypt$<n>$<r>$<p>$<key-length>`
+    - `bcrypt$<cost>`
+  - The defaults are:
+    - `argon2`:    `argon2$2$65536$8$50`
+    - `bcrypt`:    `bcrypt$10`
+    - `scrypt`:    `scrypt$65536$16$2$50`
+    - `pbkdf2`:    `pbkdf2$320000$50`
+    - `pbkdf2_v1`: `pbkdf2$10000$50`
+    - `pbkdf2_v2`: `pbkdf2$320000$50`
+  - Adjusting the algorithm parameters using this functionality is done at your own risk.
 - `CSRF_COOKIE_HTTP_ONLY`: **true**: Set false to allow JavaScript to read CSRF cookie.
 - `MIN_PASSWORD_LENGTH`: **6**: Minimum password length for new users.
 - `PASSWORD_COMPLEXITY`: **off**: Comma separated list of character classes required to pass minimum complexity. If left empty or no valid values are specified, checking is disabled (off):
diff --git a/models/user/user.go b/models/user/user.go
index 7e896e26dab7b..f6fafe64f3915 100644
--- a/models/user/user.go
+++ b/models/user/user.go
@@ -6,8 +6,6 @@ package user
 
 import (
 	"context"
-	"crypto/sha256"
-	"crypto/subtle"
 	"encoding/hex"
 	"fmt"
 	"net/url"
@@ -21,6 +19,7 @@ import (
 	"code.gitea.io/gitea/models/auth"
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/modules/auth/openid"
+	"code.gitea.io/gitea/modules/auth/password/hash"
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/log"
@@ -30,10 +29,6 @@ import (
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/validation"
 
-	"golang.org/x/crypto/argon2"
-	"golang.org/x/crypto/bcrypt"
-	"golang.org/x/crypto/pbkdf2"
-	"golang.org/x/crypto/scrypt"
 	"xorm.io/builder"
 )
 
@@ -48,21 +43,6 @@ const (
 	UserTypeOrganization
 )
 
-const (
-	algoBcrypt = "bcrypt"
-	algoScrypt = "scrypt"
-	algoArgon2 = "argon2"
-	algoPbkdf2 = "pbkdf2"
-)
-
-// AvailableHashAlgorithms represents the available password hashing algorithms
-var AvailableHashAlgorithms = []string{
-	algoPbkdf2,
-	algoArgon2,
-	algoScrypt,
-	algoBcrypt,
-}
-
 const (
 	// EmailNotificationsEnabled indicates that the user would like to receive all email notifications except your own
 	EmailNotificationsEnabled = "enabled"
@@ -377,42 +357,6 @@ func (u *User) NewGitSig() *git.Signature {
 	}
 }
 
-func hashPassword(passwd, salt, algo string) (string, error) {
-	var tempPasswd []byte
-	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 but Authenticate should
-	// regenerate the Salt following a successful validation.
-	if len(salt) == 10 {
-		saltBytes = []byte(salt)
-	} else {
-		var err error
-		saltBytes, err = hex.DecodeString(salt)
-		if err != nil {
-			return "", err
-		}
-	}
-
-	switch algo {
-	case algoBcrypt:
-		tempPasswd, _ = bcrypt.GenerateFromPassword([]byte(passwd), bcrypt.DefaultCost)
-		return string(tempPasswd), nil
-	case algoScrypt:
-		tempPasswd, _ = scrypt.Key([]byte(passwd), saltBytes, 65536, 16, 2, 50)
-	case algoArgon2:
-		tempPasswd = argon2.IDKey([]byte(passwd), saltBytes, 2, 65536, 8, 50)
-	case algoPbkdf2:
-		fallthrough
-	default:
-		tempPasswd = pbkdf2.Key([]byte(passwd), saltBytes, 10000, 50, sha256.New)
-	}
-
-	return hex.EncodeToString(tempPasswd), nil
-}
-
 // SetPassword hashes a password using the algorithm defined in the config value of PASSWORD_HASH_ALGO
 // change passwd, salt and passwd_hash_algo fields
 func (u *User) SetPassword(passwd string) (err error) {
@@ -426,7 +370,7 @@ func (u *User) SetPassword(passwd string) (err error) {
 	if u.Salt, err = GetUserSalt(); err != nil {
 		return err
 	}
-	if u.Passwd, err = hashPassword(passwd, u.Salt, setting.PasswordHashAlgo); err != nil {
+	if u.Passwd, err = hash.Parse(setting.PasswordHashAlgo).Hash(passwd, u.Salt); err != nil {
 		return err
 	}
 	u.PasswdHashAlgo = setting.PasswordHashAlgo
@@ -434,20 +378,9 @@ func (u *User) SetPassword(passwd string) (err error) {
 	return nil
 }
 
-// ValidatePassword checks if given password matches the one belongs to the user.
+// ValidatePassword checks if the given password matches the one belonging to the user.
 func (u *User) ValidatePassword(passwd string) bool {
-	tempHash, err := hashPassword(passwd, u.Salt, u.PasswdHashAlgo)
-	if err != nil {
-		return false
-	}
-
-	if u.PasswdHashAlgo != algoBcrypt && subtle.ConstantTimeCompare([]byte(u.Passwd), []byte(tempHash)) == 1 {
-		return true
-	}
-	if u.PasswdHashAlgo == algoBcrypt && bcrypt.CompareHashAndPassword([]byte(u.Passwd), []byte(passwd)) == nil {
-		return true
-	}
-	return false
+	return hash.Parse(u.PasswdHashAlgo).VerifyPassword(passwd, u.Passwd, u.Salt)
 }
 
 // IsPasswordSet checks if the password is set or left empty
diff --git a/models/user/user_test.go b/models/user/user_test.go
index 7a58d2f822cb8..fc8f6b8d515ce 100644
--- a/models/user/user_test.go
+++ b/models/user/user_test.go
@@ -14,6 +14,7 @@ import (
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/auth/password/hash"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/timeutil"
@@ -164,7 +165,7 @@ func TestEmailNotificationPreferences(t *testing.T) {
 func TestHashPasswordDeterministic(t *testing.T) {
 	b := make([]byte, 16)
 	u := &user_model.User{}
-	algos := []string{"argon2", "pbkdf2", "scrypt", "bcrypt"}
+	algos := hash.RecommendedHashAlgorithms
 	for j := 0; j < len(algos); j++ {
 		u.PasswdHashAlgo = algos[j]
 		for i := 0; i < 50; i++ {
diff --git a/modules/auth/password/hash/argon2.go b/modules/auth/password/hash/argon2.go
new file mode 100644
index 0000000000000..22bc5a32d8b2d
--- /dev/null
+++ b/modules/auth/password/hash/argon2.go
@@ -0,0 +1,76 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package hash
+
+import (
+	"encoding/hex"
+	"strings"
+
+	"code.gitea.io/gitea/modules/log"
+	"golang.org/x/crypto/argon2"
+)
+
+func init() {
+	registerHasher("argon2", NewArgon2Hasher)
+}
+
+// Argon2Hasher implements PasswordHasher
+// and uses the Argon2 key derivation function, hybrant variant
+type Argon2Hasher struct {
+	time    uint32
+	memory  uint32
+	threads uint8
+	keyLen  uint32
+}
+
+// HashWithSaltBytes a provided password and salt
+func (hasher *Argon2Hasher) HashWithSaltBytes(password string, salt []byte) string {
+	if hasher == nil {
+		return ""
+	}
+	return hex.EncodeToString(argon2.IDKey([]byte(password), salt, hasher.time, hasher.memory, hasher.threads, hasher.keyLen))
+}
+
+// NewArgon2Hasher is a factory method to create an Argon2Hasher
+// The provided config should be either empty or of the form:
+// "<time>$<memory>$<threads>$<keyLen>", where <x> is the string representation
+// of an integer
+func NewArgon2Hasher(config string) *Argon2Hasher {
+	// This default configuration uses the following parameters:
+	// time=2, memory=64*1024, threads=8, keyLen=50.
+	// It will make two passes through the memory, using 64MiB in total.
+	hasher := &Argon2Hasher{
+		time:    2,
+		memory:  1 << 16,
+		threads: 8,
+		keyLen:  50,
+	}
+
+	if config == "" {
+		return hasher
+	}
+
+	vals := strings.SplitN(config, "$", 4)
+	if len(vals) != 4 {
+		log.Error("invalid argon2 hash spec %s", config)
+		return nil
+	}
+
+	parsed, err := parseUIntParam(vals[0], "time", "argon2", config, nil)
+	hasher.time = uint32(parsed)
+
+	parsed, err = parseUIntParam(vals[1], "memory", "argon2", config, err)
+	hasher.memory = uint32(parsed)
+
+	parsed, err = parseUIntParam(vals[2], "threads", "argon2", config, err)
+	hasher.threads = uint8(parsed)
+
+	parsed, err = parseUIntParam(vals[3], "keyLen", "argon2", config, err)
+	hasher.keyLen = uint32(parsed)
+	if err != nil {
+		return nil
+	}
+
+	return hasher
+}
diff --git a/modules/auth/password/hash/bcrypt.go b/modules/auth/password/hash/bcrypt.go
new file mode 100644
index 0000000000000..86dfad71f23fd
--- /dev/null
+++ b/modules/auth/password/hash/bcrypt.go
@@ -0,0 +1,51 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package hash
+
+import (
+	"golang.org/x/crypto/bcrypt"
+)
+
+func init() {
+	registerHasher("bcrypt", NewBcryptHasher)
+}
+
+// BcryptHasher implements PasswordHasher
+// and uses the bcrypt password hash function.
+type BcryptHasher struct {
+	cost int
+}
+
+// HashWithSaltBytes a provided password and salt
+func (hasher *BcryptHasher) HashWithSaltBytes(password string, salt []byte) string {
+	if hasher == nil {
+		return ""
+	}
+	hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(password), hasher.cost)
+	return string(hashedPassword)
+}
+
+func (hasher *BcryptHasher) VerifyPassword(password, hashedPassword, salt string) bool {
+	return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)) == nil
+}
+
+// NewBcryptHasher is a factory method to create an BcryptHasher
+// The provided config should be either empty or the string representation of the "<cost>"
+// as an integer
+func NewBcryptHasher(config string) *BcryptHasher {
+	hasher := &BcryptHasher{
+		cost: 10, // cost=10. i.e. 2^10 rounds of key expansion.
+	}
+
+	if config == "" {
+		return hasher
+	}
+	var err error
+	hasher.cost, err = parseIntParam(config, "cost", "bcrypt", config, nil)
+	if err != nil {
+		return nil
+	}
+
+	return hasher
+}
diff --git a/modules/auth/password/hash/common.go b/modules/auth/password/hash/common.go
new file mode 100644
index 0000000000000..ac6faf35cff96
--- /dev/null
+++ b/modules/auth/password/hash/common.go
@@ -0,0 +1,28 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package hash
+
+import (
+	"strconv"
+
+	"code.gitea.io/gitea/modules/log"
+)
+
+func parseIntParam(value, param, algorithmName, config string, previousErr error) (int, error) {
+	parsed, err := strconv.Atoi(value)
+	if err != nil {
+		log.Error("invalid integer for %s representation in %s hash spec %s", param, algorithmName, config)
+		return 0, err
+	}
+	return parsed, previousErr // <- Keep the previous error as this function should still return an error once everything has been checked if any call failed
+}
+
+func parseUIntParam(value, param, algorithmName, config string, previousErr error) (uint64, error) {
+	parsed, err := strconv.ParseUint(value, 10, 64)
+	if err != nil {
+		log.Error("invalid integer for %s representation in %s hash spec %s", param, algorithmName, config)
+		return 0, err
+	}
+	return parsed, previousErr // <- Keep the previous error as this function should still return an error once everything has been checked if any call failed
+}
diff --git a/modules/auth/password/hash/hash.go b/modules/auth/password/hash/hash.go
new file mode 100644
index 0000000000000..ab0350865c5a9
--- /dev/null
+++ b/modules/auth/password/hash/hash.go
@@ -0,0 +1,147 @@
+// 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
+	Name string
+}
+
+// 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 {
+	// The bcrypt package has its own specialized compare function that takes into
+	// account the stored password's bcrypt parameters.
+	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.Name, 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{}
+)
+
+func registerHasher[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 could be empty
+// At that point the default was `pbkdf2` without configuration values
+// Please note this is not the same as the DefaultAlgorithm
+const defaultEmptyHashAlgorithmName = "pbkdf2"
+
+func Parse(algorithm string) *PasswordHashAlgorithm {
+	if algorithm == "" {
+		algorithm = defaultEmptyHashAlgorithmName
+	}
+
+	if DefaultHashAlgorithm != nil && algorithm == DefaultHashAlgorithm.Name {
+		return DefaultHashAlgorithm
+	}
+
+	ptr := lastNonDefaultAlgorithm.Load()
+	if ptr != nil {
+		hashAlgorithm, ok := ptr.(*PasswordHashAlgorithm)
+		if ok && hashAlgorithm.Name == algorithm {
+			return hashAlgorithm
+		}
+	}
+
+	vals := strings.SplitN(algorithm, "$", 2)
+	var name string
+	var config string
+	if len(vals) == 0 {
+		return nil
+	}
+	if len(vals) > 0 {
+		name = vals[0]
+	}
+	if len(vals) > 1 {
+		config = vals[1]
+	}
+	newFn, has := availableHasherFactories[name]
+	if !has {
+		return nil
+	}
+	ph := newFn(config)
+	if ph == nil {
+		return nil
+	}
+	hashAlgorithm := &PasswordHashAlgorithm{
+		PasswordSaltHasher: ph,
+		Name:               algorithm,
+	}
+
+	lastNonDefaultAlgorithm.Store(hashAlgorithm)
+
+	return hashAlgorithm
+}
diff --git a/modules/auth/password/hash/hash_test.go b/modules/auth/password/hash/hash_test.go
new file mode 100644
index 0000000000000..992e9690de352
--- /dev/null
+++ b/modules/auth/password/hash/hash_test.go
@@ -0,0 +1,186 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package hash
+
+import (
+	"encoding/hex"
+	"strconv"
+	"strings"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+type testSaltHasher string
+
+func (t testSaltHasher) HashWithSaltBytes(password string, salt []byte) string {
+	return password + "$" + string(salt) + "$" + string(t)
+}
+
+func Test_registerHasher(t *testing.T) {
+	registerHasher("Test_registerHasher", func(config string) testSaltHasher {
+		return testSaltHasher(config)
+	})
+
+	assert.Panics(t, func() {
+		registerHasher("Test_registerHasher", func(config string) testSaltHasher {
+			return testSaltHasher(config)
+		})
+	})
+
+	assert.Equal(t, "password$salt$",
+		Parse("Test_registerHasher").PasswordSaltHasher.HashWithSaltBytes("password", []byte("salt")))
+
+	assert.Equal(t, "password$salt$config",
+		Parse("Test_registerHasher$config").PasswordSaltHasher.HashWithSaltBytes("password", []byte("salt")))
+
+	delete(availableHasherFactories, "Test_registerHasher")
+}
+
+func TestParse(t *testing.T) {
+	hashAlgorithmsToTest := []string{}
+	for plainHashAlgorithmNames := range availableHasherFactories {
+		hashAlgorithmsToTest = append(hashAlgorithmsToTest, plainHashAlgorithmNames)
+	}
+	for _, aliased := range aliasAlgorithmNames {
+		if strings.Contains(aliased, "$") {
+			hashAlgorithmsToTest = append(hashAlgorithmsToTest, aliased)
+		}
+	}
+	for _, algorithmName := range hashAlgorithmsToTest {
+		t.Run(algorithmName, func(t *testing.T) {
+			algo := Parse(algorithmName)
+			assert.NotNil(t, algo, "Algorithm %s resulted in an empty algorithm", algorithmName)
+		})
+	}
+}
+
+func TestHashing(t *testing.T) {
+	hashAlgorithmsToTest := []string{}
+	for plainHashAlgorithmNames := range availableHasherFactories {
+		hashAlgorithmsToTest = append(hashAlgorithmsToTest, plainHashAlgorithmNames)
+	}
+	for _, aliased := range aliasAlgorithmNames {
+		if strings.Contains(aliased, "$") {
+			hashAlgorithmsToTest = append(hashAlgorithmsToTest, aliased)
+		}
+	}
+
+	runTests := func(password, salt string, shouldPass bool) {
+		for _, algorithmName := range hashAlgorithmsToTest {
+			t.Run(algorithmName, func(t *testing.T) {
+				output, err := Parse(algorithmName).Hash(password, salt)
+				if shouldPass {
+					assert.NoError(t, err)
+					assert.NotEmpty(t, output, "output for %s was empty", algorithmName)
+				} else {
+					assert.Error(t, err)
+				}
+
+				assert.Equal(t, Parse(algorithmName).VerifyPassword(password, output, salt), shouldPass)
+			})
+		}
+	}
+
+	// Test with new salt format.
+	runTests(strings.Repeat("a", 16), hex.EncodeToString([]byte{0x01, 0x02, 0x03}), true)
+
+	// Test with legacy salt format.
+	runTests(strings.Repeat("a", 16), strings.Repeat("b", 10), true)
+
+	// Test with invalid salt.
+	runTests(strings.Repeat("a", 16), "a", false)
+}
+
+// vectors were generated using the current codebase.
+var vectors = []struct {
+	algorithms []string
+	password   string
+	salt       string
+	output     string
+	shouldfail bool
+}{
+	{
+		algorithms: []string{"bcrypt", "bcrypt$10"},
+		password:   "abcdef",
+		salt:       strings.Repeat("a", 10),
+		output:     "$2a$10$fjtm8BsQ2crym01/piJroenO3oSVUBhSLKaGdTYJ4tG0ePVCrU0G2",
+		shouldfail: false,
+	},
+	{
+		algorithms: []string{"scrypt", "scrypt$65536$16$2$50"},
+		password:   "abcdef",
+		salt:       strings.Repeat("a", 10),
+		output:     "3b571d0c07c62d42b7bad3dbf18fb0cd67d4d8cd4ad4c6928e1090e5b2a4a84437c6fd2627d897c0e7e65025ca62b67a0002",
+		shouldfail: false,
+	},
+	{
+		algorithms: []string{"argon2", "argon2$2$65536$8$50"},
+		password:   "abcdef",
+		salt:       strings.Repeat("a", 10),
+		output:     "551f089f570f989975b6f7c6a8ff3cf89bc486dd7bbe87ed4d80ad4362f8ee599ec8dda78dac196301b98456402bcda775dc",
+		shouldfail: false,
+	},
+	{
+		algorithms: []string{"pbkdf2", "pbkdf2$10000$50"},
+		password:   "abcdef",
+		salt:       strings.Repeat("a", 10),
+		output:     "ab48d5471b7e6ed42d10001db88c852ff7303c788e49da5c3c7b63d5adf96360303724b74b679223a3dea8a242d10abb1913",
+		shouldfail: false,
+	},
+	{
+		algorithms: []string{"bcrypt", "bcrypt$10"},
+		password:   "abcdef",
+		salt:       hex.EncodeToString([]byte{0x01, 0x02, 0x03, 0x04}),
+		output:     "$2a$10$qhgm32w9ZpqLygugWJsLjey8xRGcaq9iXAfmCeNBXxddgyoaOC3Gq",
+		shouldfail: false,
+	},
+	{
+		algorithms: []string{"scrypt", "scrypt$65536$16$2$50"},
+		password:   "abcdef",
+		salt:       hex.EncodeToString([]byte{0x01, 0x02, 0x03, 0x04}),
+		output:     "25fe5f66b43fa4eb7b6717905317cd2223cf841092dc8e0a1e8c75720ad4846cb5d9387303e14bc3c69faa3b1c51ef4b7de1",
+		shouldfail: false,
+	},
+	{
+		algorithms: []string{"argon2", "argon2$2$65536$8$50"},
+		password:   "abcdef",
+		salt:       hex.EncodeToString([]byte{0x01, 0x02, 0x03, 0x04}),
+		output:     "9c287db63a91d18bb1414b703216da4fc431387c1ae7c8acdb280222f11f0929831055dbfd5126a3b48566692e83ec750d2a",
+		shouldfail: false,
+	},
+	{
+		algorithms: []string{"pbkdf2", "pbkdf2$10000$50"},
+		password:   "abcdef",
+		salt:       hex.EncodeToString([]byte{0x01, 0x02, 0x03, 0x04}),
+		output:     "45d6cdc843d65cf0eda7b90ab41435762a282f7df013477a1c5b212ba81dbdca2edf1ecc4b5cb05956bb9e0c37ab29315d78",
+		shouldfail: false,
+	},
+	{
+		algorithms: []string{"pbkdf2$320000$50"},
+		password:   "abcdef",
+		salt:       hex.EncodeToString([]byte{0x01, 0x02, 0x03, 0x04}),
+		output:     "84e233114499e8721da80e85568e5b7b5900b3e49a30845fcda9d1e1756da4547d70f8740ac2b4a5d82f88cebcd27f21bfe2",
+		shouldfail: false,
+	},
+	{
+		algorithms: []string{"pbkdf2", "pbkdf2$10000$50"},
+		password:   "abcdef",
+		salt:       "",
+		output:     "",
+		shouldfail: true,
+	},
+}
+
+// Ensure that the current code will correctly verify against the test vectors.
+func TestVectors(t *testing.T) {
+	for i, vector := range vectors {
+		for _, algorithm := range vector.algorithms {
+			t.Run(strconv.Itoa(i)+": "+algorithm, func(t *testing.T) {
+				pa := Parse(algorithm)
+				assert.Equal(t, !vector.shouldfail, pa.VerifyPassword(vector.password, vector.output, vector.salt))
+			})
+		}
+	}
+}
diff --git a/modules/auth/password/hash/pbkdf2.go b/modules/auth/password/hash/pbkdf2.go
new file mode 100644
index 0000000000000..f9f237b983bac
--- /dev/null
+++ b/modules/auth/password/hash/pbkdf2.go
@@ -0,0 +1,61 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package hash
+
+import (
+	"crypto/sha256"
+	"encoding/hex"
+	"strings"
+
+	"code.gitea.io/gitea/modules/log"
+	"golang.org/x/crypto/pbkdf2"
+)
+
+func init() {
+	registerHasher("pbkdf2", NewPBKDF2Hasher)
+}
+
+// PBKDF2Hasher implements PasswordHasher
+// and uses the PBKDF2 key derivation function.
+type PBKDF2Hasher struct {
+	iter, keyLen int
+}
+
+// HashWithSaltBytes a provided password and salt
+func (hasher *PBKDF2Hasher) HashWithSaltBytes(password string, salt []byte) string {
+	if hasher == nil {
+		return ""
+	}
+	return hex.EncodeToString(pbkdf2.Key([]byte(password), salt, hasher.iter, hasher.keyLen, sha256.New))
+}
+
+// NewPBKDF2Hasher is a factory method to create an PBKDF2Hasher
+// config should be either empty or of the form:
+// "<iter>$<keyLen>", where <x> is the string representation
+// of an integer
+func NewPBKDF2Hasher(config string) *PBKDF2Hasher {
+	hasher := &PBKDF2Hasher{
+		iter:   10_000,
+		keyLen: 50,
+	}
+
+	if config == "" {
+		return hasher
+	}
+
+	vals := strings.SplitN(config, "$", 2)
+	if len(vals) != 2 {
+		log.Error("invalid pbkdf2 hash spec %s", config)
+		return nil
+	}
+
+	var err error
+	hasher.iter, err = parseIntParam(vals[0], "iter", "pbkdf2", config, nil)
+	hasher.keyLen, err = parseIntParam(vals[1], "keyLen", "pbkdf2", config, err)
+	if err != nil {
+		return nil
+	}
+
+	return hasher
+}
diff --git a/modules/auth/password/hash/scrypt.go b/modules/auth/password/hash/scrypt.go
new file mode 100644
index 0000000000000..a95a7f13547e5
--- /dev/null
+++ b/modules/auth/password/hash/scrypt.go
@@ -0,0 +1,63 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package hash
+
+import (
+	"encoding/hex"
+	"strings"
+
+	"code.gitea.io/gitea/modules/log"
+	"golang.org/x/crypto/scrypt"
+)
+
+func init() {
+	registerHasher("scrypt", NewScryptHasher)
+}
+
+// ScryptHasher implements PasswordHasher
+// and uses the scrypt key derivation function.
+type ScryptHasher struct {
+	n, r, p, keyLen int
+}
+
+// HashWithSaltBytes a provided password and salt
+func (hasher *ScryptHasher) HashWithSaltBytes(password string, salt []byte) string {
+	if hasher == nil {
+		return ""
+	}
+	hashedPassword, _ := scrypt.Key([]byte(password), salt, hasher.n, hasher.r, hasher.p, hasher.keyLen)
+	return hex.EncodeToString(hashedPassword)
+}
+
+// NewScryptHasher is a factory method to create an ScryptHasher
+// The provided config should be either empty or of the form:
+// "<n>$<r>$<p>$<keyLen>", where <x> is the string representation
+// of an integer
+func NewScryptHasher(config string) *ScryptHasher {
+	hasher := &ScryptHasher{
+		n:      1 << 16,
+		r:      16,
+		p:      2, // 2 passes through memory - this default config will use 128MiB in total.
+		keyLen: 50,
+	}
+
+	if config == "" {
+		return hasher
+	}
+
+	vals := strings.SplitN(config, "$", 4)
+	if len(vals) != 4 {
+		log.Error("invalid scrypt hash spec %s", config)
+		return nil
+	}
+	var err error
+	hasher.n, err = parseIntParam(vals[0], "n", "scrypt", config, nil)
+	hasher.r, err = parseIntParam(vals[1], "r", "scrypt", config, err)
+	hasher.p, err = parseIntParam(vals[2], "p", "scrypt", config, err)
+	hasher.keyLen, err = parseIntParam(vals[3], "keyLen", "scrypt", config, err)
+	if err != nil {
+		return nil
+	}
+	return hasher
+}
diff --git a/modules/auth/password/hash/setting.go b/modules/auth/password/hash/setting.go
new file mode 100644
index 0000000000000..fcbce0aa3135c
--- /dev/null
+++ b/modules/auth/password/hash/setting.go
@@ -0,0 +1,40 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package hash
+
+const DefaultHashAlgorithmName = "pbkdf2"
+
+var DefaultHashAlgorithm *PasswordHashAlgorithm
+
+var aliasAlgorithmNames = map[string]string{
+	"argon2":    "argon2$2$65536$8$50",
+	"bcrypt":    "bcrypt$10",
+	"scrypt":    "scrypt$65536$16$2$50",
+	"pbkdf2":    "pbkdf2_v2", // pbkdf2 should default to pbkdf2_v2
+	"pbkdf2_v1": "pbkdf2$10000$50",
+	// The latest PBKDF2 password algorithm is used as the default since it doesn't
+	// use a lot of  memory and is safer to use on less powerful devices.
+	"pbkdf2_v2": "pbkdf2$320000$50",
+}
+
+var RecommendedHashAlgorithms = []string{
+	"pbkdf2",
+	"argon2",
+	"bcrypt",
+	"scrypt",
+}
+
+func SetDefaultPasswordHashAlgorithm(algorithmName string) (string, *PasswordHashAlgorithm) {
+	if algorithmName == "" {
+		algorithmName = DefaultHashAlgorithmName
+	}
+	alias, has := aliasAlgorithmNames[algorithmName]
+	for has {
+		algorithmName = alias
+		alias, has = aliasAlgorithmNames[algorithmName]
+	}
+	DefaultHashAlgorithm = Parse(algorithmName)
+
+	return algorithmName, DefaultHashAlgorithm
+}
diff --git a/modules/auth/password/hash/setting_test.go b/modules/auth/password/hash/setting_test.go
new file mode 100644
index 0000000000000..4c20ff179b3e5
--- /dev/null
+++ b/modules/auth/password/hash/setting_test.go
@@ -0,0 +1,38 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package hash
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestCheckSettingPasswordHashAlgorithm(t *testing.T) {
+	t.Run("pbkdf2 is pbkdf2_v2", func(t *testing.T) {
+		pbkdf2v2Config, pbkdf2v2Algo := SetDefaultPasswordHashAlgorithm("pbkdf2_v2")
+		pbkdf2Config, pbkdf2Algo := SetDefaultPasswordHashAlgorithm("pbkdf2")
+
+		assert.Equal(t, pbkdf2v2Config, pbkdf2Config)
+		assert.Equal(t, pbkdf2v2Algo.Name, pbkdf2Algo.Name)
+	})
+
+	for a, b := range aliasAlgorithmNames {
+		t.Run(a+"="+b, func(t *testing.T) {
+			aConfig, aAlgo := SetDefaultPasswordHashAlgorithm(a)
+			bConfig, bAlgo := SetDefaultPasswordHashAlgorithm(b)
+
+			assert.Equal(t, bConfig, aConfig)
+			assert.Equal(t, aAlgo.Name, bAlgo.Name)
+		})
+	}
+
+	t.Run("pbkdf2_v2 is the default when default password hash algorithm is empty", func(t *testing.T) {
+		emptyConfig, emptyAlgo := SetDefaultPasswordHashAlgorithm("")
+		pbkdf2v2Config, pbkdf2v2Algo := SetDefaultPasswordHashAlgorithm("pbkdf2_v2")
+
+		assert.Equal(t, pbkdf2v2Config, emptyConfig)
+		assert.Equal(t, pbkdf2v2Algo.Name, emptyAlgo.Name)
+	})
+}
diff --git a/modules/password/password.go b/modules/auth/password/password.go
similarity index 93%
rename from modules/password/password.go
rename to modules/auth/password/password.go
index fe2a2a7bd50f9..2172dc8b446cf 100644
--- a/modules/password/password.go
+++ b/modules/auth/password/password.go
@@ -11,8 +11,8 @@ import (
 	"strings"
 	"sync"
 
-	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/translation"
 )
 
 // complexity contains information about a particular kind of password complexity
@@ -112,13 +112,13 @@ func Generate(n int) (string, error) {
 }
 
 // BuildComplexityError builds the error message when password complexity checks fail
-func BuildComplexityError(ctx *context.Context) string {
+func BuildComplexityError(locale translation.Locale) string {
 	var buffer bytes.Buffer
-	buffer.WriteString(ctx.Tr("form.password_complexity"))
+	buffer.WriteString(locale.Tr("form.password_complexity"))
 	buffer.WriteString("<ul>")
 	for _, c := range requiredList {
 		buffer.WriteString("<li>")
-		buffer.WriteString(ctx.Tr(c.TrNameOne))
+		buffer.WriteString(locale.Tr(c.TrNameOne))
 		buffer.WriteString("</li>")
 	}
 	buffer.WriteString("</ul>")
diff --git a/modules/password/password_test.go b/modules/auth/password/password_test.go
similarity index 100%
rename from modules/password/password_test.go
rename to modules/auth/password/password_test.go
diff --git a/modules/password/pwn.go b/modules/auth/password/pwn.go
similarity index 93%
rename from modules/password/pwn.go
rename to modules/auth/password/pwn.go
index 91bad0d25b28a..df425ac65941b 100644
--- a/modules/password/pwn.go
+++ b/modules/auth/password/pwn.go
@@ -6,7 +6,7 @@ package password
 import (
 	"context"
 
-	"code.gitea.io/gitea/modules/password/pwn"
+	"code.gitea.io/gitea/modules/auth/password/pwn"
 	"code.gitea.io/gitea/modules/setting"
 )
 
diff --git a/modules/password/pwn/pwn.go b/modules/auth/password/pwn/pwn.go
similarity index 100%
rename from modules/password/pwn/pwn.go
rename to modules/auth/password/pwn/pwn.go
diff --git a/modules/password/pwn/pwn_test.go b/modules/auth/password/pwn/pwn_test.go
similarity index 100%
rename from modules/password/pwn/pwn_test.go
rename to modules/auth/password/pwn/pwn_test.go
diff --git a/modules/setting/setting.go b/modules/setting/setting.go
index a68a46f7add10..0cd2db356d8a0 100644
--- a/modules/setting/setting.go
+++ b/modules/setting/setting.go
@@ -21,6 +21,7 @@ import (
 	"text/template"
 	"time"
 
+	"code.gitea.io/gitea/modules/auth/password/hash"
 	"code.gitea.io/gitea/modules/container"
 	"code.gitea.io/gitea/modules/generate"
 	"code.gitea.io/gitea/modules/json"
@@ -968,7 +969,14 @@ func loadFromConf(allowEmpty bool, extraConfig string) {
 	DisableGitHooks = sec.Key("DISABLE_GIT_HOOKS").MustBool(true)
 	DisableWebhooks = sec.Key("DISABLE_WEBHOOKS").MustBool(false)
 	OnlyAllowPushIfGiteaEnvironmentSet = sec.Key("ONLY_ALLOW_PUSH_IF_GITEA_ENVIRONMENT_SET").MustBool(true)
-	PasswordHashAlgo = sec.Key("PASSWORD_HASH_ALGO").MustString("pbkdf2")
+
+	// Ensure that the provided default hash algorithm is a valid hash algorithm
+	var algorithm *hash.PasswordHashAlgorithm
+	PasswordHashAlgo, algorithm = hash.SetDefaultPasswordHashAlgorithm(sec.Key("PASSWORD_HASH_ALGO").MustString(""))
+	if algorithm == nil {
+		log.Fatal("The provided password hash algorithm was invalid: %s", sec.Key("PASSWORD_HASH_ALGO").MustString(""))
+	}
+
 	CSRFCookieHTTPOnly = sec.Key("CSRF_COOKIE_HTTP_ONLY").MustBool(true)
 	PasswordCheckPwn = sec.Key("PASSWORD_CHECK_PWN").MustBool(false)
 	SuccessfulTokensCacheSize = sec.Key("SUCCESSFUL_TOKENS_CACHE_SIZE").MustInt(20)
diff --git a/routers/api/v1/admin/user.go b/routers/api/v1/admin/user.go
index 75d5520a0eee4..1fbdab3e5598a 100644
--- a/routers/api/v1/admin/user.go
+++ b/routers/api/v1/admin/user.go
@@ -15,9 +15,9 @@ import (
 	"code.gitea.io/gitea/models/auth"
 	"code.gitea.io/gitea/models/db"
 	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/auth/password"
 	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
-	"code.gitea.io/gitea/modules/password"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/timeutil"
diff --git a/routers/install/install.go b/routers/install/install.go
index 5d7ecff48f86f..34ed5c355b823 100644
--- a/routers/install/install.go
+++ b/routers/install/install.go
@@ -20,6 +20,7 @@ import (
 	"code.gitea.io/gitea/models/migrations"
 	system_model "code.gitea.io/gitea/models/system"
 	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/auth/password/hash"
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/generate"
@@ -79,7 +80,7 @@ func Init(ctx goctx.Context) func(next http.Handler) http.Handler {
 					"AllLangs":      translation.AllLangs(),
 					"PageStartTime": startTime,
 
-					"PasswordHashAlgorithms": user_model.AvailableHashAlgorithms,
+					"PasswordHashAlgorithms": hash.RecommendedHashAlgorithms,
 				},
 			}
 			defer ctx.Close()
diff --git a/routers/web/admin/users.go b/routers/web/admin/users.go
index 38969dadaadc8..1bb9d0480682e 100644
--- a/routers/web/admin/users.go
+++ b/routers/web/admin/users.go
@@ -14,10 +14,10 @@ import (
 	"code.gitea.io/gitea/models/auth"
 	"code.gitea.io/gitea/models/db"
 	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/auth/password"
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
-	"code.gitea.io/gitea/modules/password"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go
index 48b7dc6862ae4..5fba632817e62 100644
--- a/routers/web/auth/auth.go
+++ b/routers/web/auth/auth.go
@@ -13,11 +13,11 @@ import (
 	"code.gitea.io/gitea/models/auth"
 	"code.gitea.io/gitea/models/db"
 	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/auth/password"
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/eventsource"
 	"code.gitea.io/gitea/modules/log"
-	"code.gitea.io/gitea/modules/password"
 	"code.gitea.io/gitea/modules/session"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/timeutil"
diff --git a/routers/web/auth/password.go b/routers/web/auth/password.go
index 16628645d7288..a5aa9c5344ddb 100644
--- a/routers/web/auth/password.go
+++ b/routers/web/auth/password.go
@@ -9,10 +9,10 @@ import (
 
 	"code.gitea.io/gitea/models/auth"
 	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/auth/password"
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
-	"code.gitea.io/gitea/modules/password"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/web"
diff --git a/routers/web/user/setting/account.go b/routers/web/user/setting/account.go
index dbaa8fd351b29..0e48013b04da1 100644
--- a/routers/web/user/setting/account.go
+++ b/routers/web/user/setting/account.go
@@ -11,10 +11,10 @@ import (
 
 	"code.gitea.io/gitea/models"
 	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/auth/password"
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
-	"code.gitea.io/gitea/modules/password"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/web"

From 39b554a7b3d34e12f92a9595945f18b00b8f99a2 Mon Sep 17 00:00:00 2001
From: Andrew Thornton <art27@cantab.net>
Date: Thu, 16 Feb 2023 19:17:44 +0000
Subject: [PATCH 02/10] prod drone


From d79c54621284a425e4a0624b484f030fa8ccd326 Mon Sep 17 00:00:00 2001
From: Andrew Thornton <art27@cantab.net>
Date: Thu, 16 Feb 2023 19:31:10 +0000
Subject: [PATCH 03/10] placate the linter

Signed-off-by: Andrew Thornton <art27@cantab.net>
---
 modules/auth/password/hash/argon2.go | 1 +
 modules/auth/password/hash/pbkdf2.go | 1 +
 modules/auth/password/hash/scrypt.go | 1 +
 3 files changed, 3 insertions(+)

diff --git a/modules/auth/password/hash/argon2.go b/modules/auth/password/hash/argon2.go
index 22bc5a32d8b2d..78bd5f88670f5 100644
--- a/modules/auth/password/hash/argon2.go
+++ b/modules/auth/password/hash/argon2.go
@@ -8,6 +8,7 @@ import (
 	"strings"
 
 	"code.gitea.io/gitea/modules/log"
+
 	"golang.org/x/crypto/argon2"
 )
 
diff --git a/modules/auth/password/hash/pbkdf2.go b/modules/auth/password/hash/pbkdf2.go
index f9f237b983bac..876bba4f27aff 100644
--- a/modules/auth/password/hash/pbkdf2.go
+++ b/modules/auth/password/hash/pbkdf2.go
@@ -9,6 +9,7 @@ import (
 	"strings"
 
 	"code.gitea.io/gitea/modules/log"
+
 	"golang.org/x/crypto/pbkdf2"
 )
 
diff --git a/modules/auth/password/hash/scrypt.go b/modules/auth/password/hash/scrypt.go
index a95a7f13547e5..e467d119bfbac 100644
--- a/modules/auth/password/hash/scrypt.go
+++ b/modules/auth/password/hash/scrypt.go
@@ -8,6 +8,7 @@ import (
 	"strings"
 
 	"code.gitea.io/gitea/modules/log"
+
 	"golang.org/x/crypto/scrypt"
 )
 

From 309ea1d3b49cf945e5ccbb672fe0c8bbae21384b Mon Sep 17 00:00:00 2001
From: zeripath <art27@cantab.net>
Date: Thu, 16 Feb 2023 19:39:00 +0000
Subject: [PATCH 04/10] as per delvh

Co-authored-by: delvh <dev.lh@web.de>
---
 docs/content/doc/advanced/config-cheat-sheet.en-us.md | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md
index 150acb7ecbf60..aa0481f8b4249 100644
--- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md
+++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md
@@ -572,16 +572,16 @@ Certain queues have defaults that override the defaults set in `[queue]` (this o
   - Note: The default parameters for `pbkdf2` hashing have changed - the previous settings are available as `pbkdf2_v1` but are not recommended.
   - The hash functions may be tuned by using `$` after the algorithm:
     - `argon2$<time>$<memory>$<threads>$<key-length>`
+    - `bcrypt$<cost>`
     - `pbkdf2$<iterations>$<key-length>`
     - `scrypt$<n>$<r>$<p>$<key-length>`
-    - `bcrypt$<cost>`
   - The defaults are:
     - `argon2`:    `argon2$2$65536$8$50`
     - `bcrypt`:    `bcrypt$10`
-    - `scrypt`:    `scrypt$65536$16$2$50`
     - `pbkdf2`:    `pbkdf2$320000$50`
     - `pbkdf2_v1`: `pbkdf2$10000$50`
     - `pbkdf2_v2`: `pbkdf2$320000$50`
+    - `scrypt`:    `scrypt$65536$16$2$50`
   - Adjusting the algorithm parameters using this functionality is done at your own risk.
 - `CSRF_COOKIE_HTTP_ONLY`: **true**: Set false to allow JavaScript to read CSRF cookie.
 - `MIN_PASSWORD_LENGTH`: **6**: Minimum password length for new users.

From 47b282eb480fd4a02a9f21854b95e4bbf6f887c6 Mon Sep 17 00:00:00 2001
From: Andrew Thornton <art27@cantab.net>
Date: Thu, 16 Feb 2023 21:58:13 +0000
Subject: [PATCH 05/10] stop-using-argon2 in test hashes

Signed-off-by: Andrew Thornton <art27@cantab.net>
---
 models/fixtures/user.yml | 130 +++++++++++++++++++--------------------
 1 file changed, 65 insertions(+), 65 deletions(-)

diff --git a/models/fixtures/user.yml b/models/fixtures/user.yml
index 63a5e0f890552..ec22f77c1a0fa 100644
--- a/models/fixtures/user.yml
+++ b/models/fixtures/user.yml
@@ -8,8 +8,8 @@
   email: user1@example.com
   keep_email_private: false
   email_notifications_preference: enabled
-  passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
-  passwd_hash_algo: argon2
+  passwd: b4910a161a57b686958f6aa711b16316e10a76b3b8c15d2d76b76d5a6071ee2f2e761d85092f27a7170d6f063da5606bcd3c
+  passwd_hash_algo: pbkdf2$320000$50
   must_change_password: false
   login_source: 0
   login_name: user1
@@ -45,8 +45,8 @@
   email: user2@example.com
   keep_email_private: true
   email_notifications_preference: enabled
-  passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
-  passwd_hash_algo: argon2
+  passwd: b4910a161a57b686958f6aa711b16316e10a76b3b8c15d2d76b76d5a6071ee2f2e761d85092f27a7170d6f063da5606bcd3c
+  passwd_hash_algo: pbkdf2$320000$50
   must_change_password: false
   login_source: 0
   login_name: user2
@@ -82,8 +82,8 @@
   email: user3@example.com
   keep_email_private: false
   email_notifications_preference: onmention
-  passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
-  passwd_hash_algo: argon2
+  passwd: b4910a161a57b686958f6aa711b16316e10a76b3b8c15d2d76b76d5a6071ee2f2e761d85092f27a7170d6f063da5606bcd3c
+  passwd_hash_algo: pbkdf2$320000$50
   must_change_password: false
   login_source: 0
   login_name: user3
@@ -119,8 +119,8 @@
   email: user4@example.com
   keep_email_private: false
   email_notifications_preference: onmention
-  passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
-  passwd_hash_algo: argon2
+  passwd: b4910a161a57b686958f6aa711b16316e10a76b3b8c15d2d76b76d5a6071ee2f2e761d85092f27a7170d6f063da5606bcd3c
+  passwd_hash_algo: pbkdf2$320000$50
   must_change_password: false
   login_source: 0
   login_name: user4
@@ -156,8 +156,8 @@
   email: user5@example.com
   keep_email_private: false
   email_notifications_preference: enabled
-  passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
-  passwd_hash_algo: argon2
+  passwd: b4910a161a57b686958f6aa711b16316e10a76b3b8c15d2d76b76d5a6071ee2f2e761d85092f27a7170d6f063da5606bcd3c
+  passwd_hash_algo: pbkdf2$320000$50
   must_change_password: false
   login_source: 0
   login_name: user5
@@ -193,8 +193,8 @@
   email: user6@example.com
   keep_email_private: false
   email_notifications_preference: enabled
-  passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
-  passwd_hash_algo: argon2
+  passwd: b4910a161a57b686958f6aa711b16316e10a76b3b8c15d2d76b76d5a6071ee2f2e761d85092f27a7170d6f063da5606bcd3c
+  passwd_hash_algo: pbkdf2$320000$50
   must_change_password: false
   login_source: 0
   login_name: user6
@@ -230,8 +230,8 @@
   email: user7@example.com
   keep_email_private: false
   email_notifications_preference: disabled
-  passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
-  passwd_hash_algo: argon2
+  passwd: b4910a161a57b686958f6aa711b16316e10a76b3b8c15d2d76b76d5a6071ee2f2e761d85092f27a7170d6f063da5606bcd3c
+  passwd_hash_algo: pbkdf2$320000$50
   must_change_password: false
   login_source: 0
   login_name: user7
@@ -267,8 +267,8 @@
   email: user8@example.com
   keep_email_private: false
   email_notifications_preference: enabled
-  passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
-  passwd_hash_algo: argon2
+  passwd: b4910a161a57b686958f6aa711b16316e10a76b3b8c15d2d76b76d5a6071ee2f2e761d85092f27a7170d6f063da5606bcd3c
+  passwd_hash_algo: pbkdf2$320000$50
   must_change_password: false
   login_source: 0
   login_name: user8
@@ -304,8 +304,8 @@
   email: user9@example.com
   keep_email_private: false
   email_notifications_preference: onmention
-  passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
-  passwd_hash_algo: argon2
+  passwd: b4910a161a57b686958f6aa711b16316e10a76b3b8c15d2d76b76d5a6071ee2f2e761d85092f27a7170d6f063da5606bcd3c
+  passwd_hash_algo: pbkdf2$320000$50
   must_change_password: false
   login_source: 0
   login_name: user9
@@ -341,8 +341,8 @@
   email: user10@example.com
   keep_email_private: false
   email_notifications_preference: enabled
-  passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
-  passwd_hash_algo: argon2
+  passwd: b4910a161a57b686958f6aa711b16316e10a76b3b8c15d2d76b76d5a6071ee2f2e761d85092f27a7170d6f063da5606bcd3c
+  passwd_hash_algo: pbkdf2$320000$50
   must_change_password: false
   login_source: 0
   login_name: user10
@@ -378,8 +378,8 @@
   email: user11@example.com
   keep_email_private: false
   email_notifications_preference: enabled
-  passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
-  passwd_hash_algo: argon2
+  passwd: b4910a161a57b686958f6aa711b16316e10a76b3b8c15d2d76b76d5a6071ee2f2e761d85092f27a7170d6f063da5606bcd3c
+  passwd_hash_algo: pbkdf2$320000$50
   must_change_password: false
   login_source: 0
   login_name: user11
@@ -415,8 +415,8 @@
   email: user12@example.com
   keep_email_private: false
   email_notifications_preference: enabled
-  passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
-  passwd_hash_algo: argon2
+  passwd: b4910a161a57b686958f6aa711b16316e10a76b3b8c15d2d76b76d5a6071ee2f2e761d85092f27a7170d6f063da5606bcd3c
+  passwd_hash_algo: pbkdf2$320000$50
   must_change_password: false
   login_source: 0
   login_name: user12
@@ -452,8 +452,8 @@
   email: user13@example.com
   keep_email_private: false
   email_notifications_preference: enabled
-  passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
-  passwd_hash_algo: argon2
+  passwd: b4910a161a57b686958f6aa711b16316e10a76b3b8c15d2d76b76d5a6071ee2f2e761d85092f27a7170d6f063da5606bcd3c
+  passwd_hash_algo: pbkdf2$320000$50
   must_change_password: false
   login_source: 0
   login_name: user13
@@ -489,8 +489,8 @@
   email: user14@example.com
   keep_email_private: false
   email_notifications_preference: enabled
-  passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
-  passwd_hash_algo: argon2
+  passwd: b4910a161a57b686958f6aa711b16316e10a76b3b8c15d2d76b76d5a6071ee2f2e761d85092f27a7170d6f063da5606bcd3c
+  passwd_hash_algo: pbkdf2$320000$50
   must_change_password: false
   login_source: 0
   login_name: user14
@@ -526,8 +526,8 @@
   email: user15@example.com
   keep_email_private: false
   email_notifications_preference: enabled
-  passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
-  passwd_hash_algo: argon2
+  passwd: b4910a161a57b686958f6aa711b16316e10a76b3b8c15d2d76b76d5a6071ee2f2e761d85092f27a7170d6f063da5606bcd3c
+  passwd_hash_algo: pbkdf2$320000$50
   must_change_password: false
   login_source: 0
   login_name: user15
@@ -563,8 +563,8 @@
   email: user16@example.com
   keep_email_private: false
   email_notifications_preference: enabled
-  passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
-  passwd_hash_algo: argon2
+  passwd: b4910a161a57b686958f6aa711b16316e10a76b3b8c15d2d76b76d5a6071ee2f2e761d85092f27a7170d6f063da5606bcd3c
+  passwd_hash_algo: pbkdf2$320000$50
   must_change_password: false
   login_source: 0
   login_name: user16
@@ -600,8 +600,8 @@
   email: user17@example.com
   keep_email_private: false
   email_notifications_preference: enabled
-  passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
-  passwd_hash_algo: argon2
+  passwd: b4910a161a57b686958f6aa711b16316e10a76b3b8c15d2d76b76d5a6071ee2f2e761d85092f27a7170d6f063da5606bcd3c
+  passwd_hash_algo: pbkdf2$320000$50
   must_change_password: false
   login_source: 0
   login_name: user17
@@ -637,8 +637,8 @@
   email: user18@example.com
   keep_email_private: false
   email_notifications_preference: enabled
-  passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
-  passwd_hash_algo: argon2
+  passwd: b4910a161a57b686958f6aa711b16316e10a76b3b8c15d2d76b76d5a6071ee2f2e761d85092f27a7170d6f063da5606bcd3c
+  passwd_hash_algo: pbkdf2$320000$50
   must_change_password: false
   login_source: 0
   login_name: user18
@@ -674,8 +674,8 @@
   email: user19@example.com
   keep_email_private: false
   email_notifications_preference: enabled
-  passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
-  passwd_hash_algo: argon2
+  passwd: b4910a161a57b686958f6aa711b16316e10a76b3b8c15d2d76b76d5a6071ee2f2e761d85092f27a7170d6f063da5606bcd3c
+  passwd_hash_algo: pbkdf2$320000$50
   must_change_password: false
   login_source: 0
   login_name: user19
@@ -711,8 +711,8 @@
   email: user20@example.com
   keep_email_private: false
   email_notifications_preference: enabled
-  passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
-  passwd_hash_algo: argon2
+  passwd: b4910a161a57b686958f6aa711b16316e10a76b3b8c15d2d76b76d5a6071ee2f2e761d85092f27a7170d6f063da5606bcd3c
+  passwd_hash_algo: pbkdf2$320000$50
   must_change_password: false
   login_source: 0
   login_name: user20
@@ -748,8 +748,8 @@
   email: user21@example.com
   keep_email_private: false
   email_notifications_preference: enabled
-  passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
-  passwd_hash_algo: argon2
+  passwd: b4910a161a57b686958f6aa711b16316e10a76b3b8c15d2d76b76d5a6071ee2f2e761d85092f27a7170d6f063da5606bcd3c
+  passwd_hash_algo: pbkdf2$320000$50
   must_change_password: false
   login_source: 0
   login_name: user21
@@ -785,8 +785,8 @@
   email: limited_org@example.com
   keep_email_private: false
   email_notifications_preference: enabled
-  passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
-  passwd_hash_algo: argon2
+  passwd: b4910a161a57b686958f6aa711b16316e10a76b3b8c15d2d76b76d5a6071ee2f2e761d85092f27a7170d6f063da5606bcd3c
+  passwd_hash_algo: pbkdf2$320000$50
   must_change_password: false
   login_source: 0
   login_name: limited_org
@@ -822,8 +822,8 @@
   email: privated_org@example.com
   keep_email_private: false
   email_notifications_preference: enabled
-  passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
-  passwd_hash_algo: argon2
+  passwd: b4910a161a57b686958f6aa711b16316e10a76b3b8c15d2d76b76d5a6071ee2f2e761d85092f27a7170d6f063da5606bcd3c
+  passwd_hash_algo: pbkdf2$320000$50
   must_change_password: false
   login_source: 0
   login_name: privated_org
@@ -859,8 +859,8 @@
   email: user24@example.com
   keep_email_private: true
   email_notifications_preference: enabled
-  passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
-  passwd_hash_algo: argon2
+  passwd: b4910a161a57b686958f6aa711b16316e10a76b3b8c15d2d76b76d5a6071ee2f2e761d85092f27a7170d6f063da5606bcd3c
+  passwd_hash_algo: pbkdf2$320000$50
   must_change_password: false
   login_source: 0
   login_name: user24
@@ -896,8 +896,8 @@
   email: org25@example.com
   keep_email_private: false
   email_notifications_preference: enabled
-  passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
-  passwd_hash_algo: argon2
+  passwd: b4910a161a57b686958f6aa711b16316e10a76b3b8c15d2d76b76d5a6071ee2f2e761d85092f27a7170d6f063da5606bcd3c
+  passwd_hash_algo: pbkdf2$320000$50
   must_change_password: false
   login_source: 0
   login_name: org25
@@ -933,8 +933,8 @@
   email: org26@example.com
   keep_email_private: false
   email_notifications_preference: onmention
-  passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
-  passwd_hash_algo: argon2
+  passwd: b4910a161a57b686958f6aa711b16316e10a76b3b8c15d2d76b76d5a6071ee2f2e761d85092f27a7170d6f063da5606bcd3c
+  passwd_hash_algo: pbkdf2$320000$50
   must_change_password: false
   login_source: 0
   login_name: org26
@@ -970,8 +970,8 @@
   email: user27@example.com
   keep_email_private: false
   email_notifications_preference: enabled
-  passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
-  passwd_hash_algo: argon2
+  passwd: b4910a161a57b686958f6aa711b16316e10a76b3b8c15d2d76b76d5a6071ee2f2e761d85092f27a7170d6f063da5606bcd3c
+  passwd_hash_algo: pbkdf2$320000$50
   must_change_password: false
   login_source: 0
   login_name: user27
@@ -1007,8 +1007,8 @@
   email: user28@example.com
   keep_email_private: true
   email_notifications_preference: enabled
-  passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
-  passwd_hash_algo: argon2
+  passwd: b4910a161a57b686958f6aa711b16316e10a76b3b8c15d2d76b76d5a6071ee2f2e761d85092f27a7170d6f063da5606bcd3c
+  passwd_hash_algo: pbkdf2$320000$50
   must_change_password: false
   login_source: 0
   login_name: user28
@@ -1044,8 +1044,8 @@
   email: user29@example.com
   keep_email_private: false
   email_notifications_preference: enabled
-  passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
-  passwd_hash_algo: argon2
+  passwd: b4910a161a57b686958f6aa711b16316e10a76b3b8c15d2d76b76d5a6071ee2f2e761d85092f27a7170d6f063da5606bcd3c
+  passwd_hash_algo: pbkdf2$320000$50
   must_change_password: false
   login_source: 0
   login_name: user29
@@ -1081,8 +1081,8 @@
   email: user30@example.com
   keep_email_private: false
   email_notifications_preference: enabled
-  passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
-  passwd_hash_algo: argon2
+  passwd: b4910a161a57b686958f6aa711b16316e10a76b3b8c15d2d76b76d5a6071ee2f2e761d85092f27a7170d6f063da5606bcd3c
+  passwd_hash_algo: pbkdf2$320000$50
   must_change_password: false
   login_source: 0
   login_name: user30
@@ -1118,8 +1118,8 @@
   email: user31@example.com
   keep_email_private: false
   email_notifications_preference: enabled
-  passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
-  passwd_hash_algo: argon2
+  passwd: b4910a161a57b686958f6aa711b16316e10a76b3b8c15d2d76b76d5a6071ee2f2e761d85092f27a7170d6f063da5606bcd3c
+  passwd_hash_algo: pbkdf2$320000$50
   must_change_password: false
   login_source: 0
   login_name: user31
@@ -1155,7 +1155,7 @@
   email: user32@example.com
   keep_email_private: false
   email_notifications_preference: enabled
-  passwd: 7d93daa0d1e6f2305cc8fa496847d61dc7320bb16262f9c55dd753480207234cdd96a93194e408341971742f4701772a025a
+  passwd: 7d93daa0d1e6f2305cc8fa496847d61dc7320bb16262f9c55dd753480207234cdd96a93194e408341971742f47017
   passwd_hash_algo: argon2
   must_change_password: false
   login_source: 0
@@ -1192,8 +1192,8 @@
   email: user33@example.com
   keep_email_private: false
   email_notifications_preference: enabled
-  passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
-  passwd_hash_algo: argon2
+  passwd: b4910a161a57b686958f6aa711b16316e10a76b3b8c15d2d76b76d5a6071ee2f2e761d85092f27a7170d6f063da5606bcd3c
+  passwd_hash_algo: pbkdf2$320000$50
   must_change_password: false
   login_source: 0
   login_name: user33

From b10e602812033c51bf731ae85f80eb63f76f79f3 Mon Sep 17 00:00:00 2001
From: Andrew Thornton <art27@cantab.net>
Date: Sat, 18 Feb 2023 12:48:03 +0000
Subject: [PATCH 06/10] Try a slightly weaker version of the pbkdf2 algorithm

Signed-off-by: Andrew Thornton <art27@cantab.net>
---
 models/fixtures/user.yml                | 128 ++++++++++++------------
 modules/auth/password/hash/argon2.go    |   2 +-
 modules/auth/password/hash/bcrypt.go    |   2 +-
 modules/auth/password/hash/hash.go      |   8 +-
 modules/auth/password/hash/hash_test.go |   9 +-
 modules/auth/password/hash/pbkdf2.go    |   2 +-
 modules/auth/password/hash/scrypt.go    |   2 +-
 modules/auth/password/hash/setting.go   |   6 +-
 8 files changed, 84 insertions(+), 75 deletions(-)

diff --git a/models/fixtures/user.yml b/models/fixtures/user.yml
index ec22f77c1a0fa..8e73bec0ca07a 100644
--- a/models/fixtures/user.yml
+++ b/models/fixtures/user.yml
@@ -8,8 +8,8 @@
   email: user1@example.com
   keep_email_private: false
   email_notifications_preference: enabled
-  passwd: b4910a161a57b686958f6aa711b16316e10a76b3b8c15d2d76b76d5a6071ee2f2e761d85092f27a7170d6f063da5606bcd3c
-  passwd_hash_algo: pbkdf2$320000$50
+  passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889
+  passwd_hash_algo: pbkdf2$50000$50
   must_change_password: false
   login_source: 0
   login_name: user1
@@ -45,8 +45,8 @@
   email: user2@example.com
   keep_email_private: true
   email_notifications_preference: enabled
-  passwd: b4910a161a57b686958f6aa711b16316e10a76b3b8c15d2d76b76d5a6071ee2f2e761d85092f27a7170d6f063da5606bcd3c
-  passwd_hash_algo: pbkdf2$320000$50
+  passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889
+  passwd_hash_algo: pbkdf2$50000$50
   must_change_password: false
   login_source: 0
   login_name: user2
@@ -82,8 +82,8 @@
   email: user3@example.com
   keep_email_private: false
   email_notifications_preference: onmention
-  passwd: b4910a161a57b686958f6aa711b16316e10a76b3b8c15d2d76b76d5a6071ee2f2e761d85092f27a7170d6f063da5606bcd3c
-  passwd_hash_algo: pbkdf2$320000$50
+  passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889
+  passwd_hash_algo: pbkdf2$50000$50
   must_change_password: false
   login_source: 0
   login_name: user3
@@ -119,8 +119,8 @@
   email: user4@example.com
   keep_email_private: false
   email_notifications_preference: onmention
-  passwd: b4910a161a57b686958f6aa711b16316e10a76b3b8c15d2d76b76d5a6071ee2f2e761d85092f27a7170d6f063da5606bcd3c
-  passwd_hash_algo: pbkdf2$320000$50
+  passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889
+  passwd_hash_algo: pbkdf2$50000$50
   must_change_password: false
   login_source: 0
   login_name: user4
@@ -156,8 +156,8 @@
   email: user5@example.com
   keep_email_private: false
   email_notifications_preference: enabled
-  passwd: b4910a161a57b686958f6aa711b16316e10a76b3b8c15d2d76b76d5a6071ee2f2e761d85092f27a7170d6f063da5606bcd3c
-  passwd_hash_algo: pbkdf2$320000$50
+  passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889
+  passwd_hash_algo: pbkdf2$50000$50
   must_change_password: false
   login_source: 0
   login_name: user5
@@ -193,8 +193,8 @@
   email: user6@example.com
   keep_email_private: false
   email_notifications_preference: enabled
-  passwd: b4910a161a57b686958f6aa711b16316e10a76b3b8c15d2d76b76d5a6071ee2f2e761d85092f27a7170d6f063da5606bcd3c
-  passwd_hash_algo: pbkdf2$320000$50
+  passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889
+  passwd_hash_algo: pbkdf2$50000$50
   must_change_password: false
   login_source: 0
   login_name: user6
@@ -230,8 +230,8 @@
   email: user7@example.com
   keep_email_private: false
   email_notifications_preference: disabled
-  passwd: b4910a161a57b686958f6aa711b16316e10a76b3b8c15d2d76b76d5a6071ee2f2e761d85092f27a7170d6f063da5606bcd3c
-  passwd_hash_algo: pbkdf2$320000$50
+  passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889
+  passwd_hash_algo: pbkdf2$50000$50
   must_change_password: false
   login_source: 0
   login_name: user7
@@ -267,8 +267,8 @@
   email: user8@example.com
   keep_email_private: false
   email_notifications_preference: enabled
-  passwd: b4910a161a57b686958f6aa711b16316e10a76b3b8c15d2d76b76d5a6071ee2f2e761d85092f27a7170d6f063da5606bcd3c
-  passwd_hash_algo: pbkdf2$320000$50
+  passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889
+  passwd_hash_algo: pbkdf2$50000$50
   must_change_password: false
   login_source: 0
   login_name: user8
@@ -304,8 +304,8 @@
   email: user9@example.com
   keep_email_private: false
   email_notifications_preference: onmention
-  passwd: b4910a161a57b686958f6aa711b16316e10a76b3b8c15d2d76b76d5a6071ee2f2e761d85092f27a7170d6f063da5606bcd3c
-  passwd_hash_algo: pbkdf2$320000$50
+  passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889
+  passwd_hash_algo: pbkdf2$50000$50
   must_change_password: false
   login_source: 0
   login_name: user9
@@ -341,8 +341,8 @@
   email: user10@example.com
   keep_email_private: false
   email_notifications_preference: enabled
-  passwd: b4910a161a57b686958f6aa711b16316e10a76b3b8c15d2d76b76d5a6071ee2f2e761d85092f27a7170d6f063da5606bcd3c
-  passwd_hash_algo: pbkdf2$320000$50
+  passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889
+  passwd_hash_algo: pbkdf2$50000$50
   must_change_password: false
   login_source: 0
   login_name: user10
@@ -378,8 +378,8 @@
   email: user11@example.com
   keep_email_private: false
   email_notifications_preference: enabled
-  passwd: b4910a161a57b686958f6aa711b16316e10a76b3b8c15d2d76b76d5a6071ee2f2e761d85092f27a7170d6f063da5606bcd3c
-  passwd_hash_algo: pbkdf2$320000$50
+  passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889
+  passwd_hash_algo: pbkdf2$50000$50
   must_change_password: false
   login_source: 0
   login_name: user11
@@ -415,8 +415,8 @@
   email: user12@example.com
   keep_email_private: false
   email_notifications_preference: enabled
-  passwd: b4910a161a57b686958f6aa711b16316e10a76b3b8c15d2d76b76d5a6071ee2f2e761d85092f27a7170d6f063da5606bcd3c
-  passwd_hash_algo: pbkdf2$320000$50
+  passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889
+  passwd_hash_algo: pbkdf2$50000$50
   must_change_password: false
   login_source: 0
   login_name: user12
@@ -452,8 +452,8 @@
   email: user13@example.com
   keep_email_private: false
   email_notifications_preference: enabled
-  passwd: b4910a161a57b686958f6aa711b16316e10a76b3b8c15d2d76b76d5a6071ee2f2e761d85092f27a7170d6f063da5606bcd3c
-  passwd_hash_algo: pbkdf2$320000$50
+  passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889
+  passwd_hash_algo: pbkdf2$50000$50
   must_change_password: false
   login_source: 0
   login_name: user13
@@ -489,8 +489,8 @@
   email: user14@example.com
   keep_email_private: false
   email_notifications_preference: enabled
-  passwd: b4910a161a57b686958f6aa711b16316e10a76b3b8c15d2d76b76d5a6071ee2f2e761d85092f27a7170d6f063da5606bcd3c
-  passwd_hash_algo: pbkdf2$320000$50
+  passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889
+  passwd_hash_algo: pbkdf2$50000$50
   must_change_password: false
   login_source: 0
   login_name: user14
@@ -526,8 +526,8 @@
   email: user15@example.com
   keep_email_private: false
   email_notifications_preference: enabled
-  passwd: b4910a161a57b686958f6aa711b16316e10a76b3b8c15d2d76b76d5a6071ee2f2e761d85092f27a7170d6f063da5606bcd3c
-  passwd_hash_algo: pbkdf2$320000$50
+  passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889
+  passwd_hash_algo: pbkdf2$50000$50
   must_change_password: false
   login_source: 0
   login_name: user15
@@ -563,8 +563,8 @@
   email: user16@example.com
   keep_email_private: false
   email_notifications_preference: enabled
-  passwd: b4910a161a57b686958f6aa711b16316e10a76b3b8c15d2d76b76d5a6071ee2f2e761d85092f27a7170d6f063da5606bcd3c
-  passwd_hash_algo: pbkdf2$320000$50
+  passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889
+  passwd_hash_algo: pbkdf2$50000$50
   must_change_password: false
   login_source: 0
   login_name: user16
@@ -600,8 +600,8 @@
   email: user17@example.com
   keep_email_private: false
   email_notifications_preference: enabled
-  passwd: b4910a161a57b686958f6aa711b16316e10a76b3b8c15d2d76b76d5a6071ee2f2e761d85092f27a7170d6f063da5606bcd3c
-  passwd_hash_algo: pbkdf2$320000$50
+  passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889
+  passwd_hash_algo: pbkdf2$50000$50
   must_change_password: false
   login_source: 0
   login_name: user17
@@ -637,8 +637,8 @@
   email: user18@example.com
   keep_email_private: false
   email_notifications_preference: enabled
-  passwd: b4910a161a57b686958f6aa711b16316e10a76b3b8c15d2d76b76d5a6071ee2f2e761d85092f27a7170d6f063da5606bcd3c
-  passwd_hash_algo: pbkdf2$320000$50
+  passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889
+  passwd_hash_algo: pbkdf2$50000$50
   must_change_password: false
   login_source: 0
   login_name: user18
@@ -674,8 +674,8 @@
   email: user19@example.com
   keep_email_private: false
   email_notifications_preference: enabled
-  passwd: b4910a161a57b686958f6aa711b16316e10a76b3b8c15d2d76b76d5a6071ee2f2e761d85092f27a7170d6f063da5606bcd3c
-  passwd_hash_algo: pbkdf2$320000$50
+  passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889
+  passwd_hash_algo: pbkdf2$50000$50
   must_change_password: false
   login_source: 0
   login_name: user19
@@ -711,8 +711,8 @@
   email: user20@example.com
   keep_email_private: false
   email_notifications_preference: enabled
-  passwd: b4910a161a57b686958f6aa711b16316e10a76b3b8c15d2d76b76d5a6071ee2f2e761d85092f27a7170d6f063da5606bcd3c
-  passwd_hash_algo: pbkdf2$320000$50
+  passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889
+  passwd_hash_algo: pbkdf2$50000$50
   must_change_password: false
   login_source: 0
   login_name: user20
@@ -748,8 +748,8 @@
   email: user21@example.com
   keep_email_private: false
   email_notifications_preference: enabled
-  passwd: b4910a161a57b686958f6aa711b16316e10a76b3b8c15d2d76b76d5a6071ee2f2e761d85092f27a7170d6f063da5606bcd3c
-  passwd_hash_algo: pbkdf2$320000$50
+  passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889
+  passwd_hash_algo: pbkdf2$50000$50
   must_change_password: false
   login_source: 0
   login_name: user21
@@ -785,8 +785,8 @@
   email: limited_org@example.com
   keep_email_private: false
   email_notifications_preference: enabled
-  passwd: b4910a161a57b686958f6aa711b16316e10a76b3b8c15d2d76b76d5a6071ee2f2e761d85092f27a7170d6f063da5606bcd3c
-  passwd_hash_algo: pbkdf2$320000$50
+  passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889
+  passwd_hash_algo: pbkdf2$50000$50
   must_change_password: false
   login_source: 0
   login_name: limited_org
@@ -822,8 +822,8 @@
   email: privated_org@example.com
   keep_email_private: false
   email_notifications_preference: enabled
-  passwd: b4910a161a57b686958f6aa711b16316e10a76b3b8c15d2d76b76d5a6071ee2f2e761d85092f27a7170d6f063da5606bcd3c
-  passwd_hash_algo: pbkdf2$320000$50
+  passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889
+  passwd_hash_algo: pbkdf2$50000$50
   must_change_password: false
   login_source: 0
   login_name: privated_org
@@ -859,8 +859,8 @@
   email: user24@example.com
   keep_email_private: true
   email_notifications_preference: enabled
-  passwd: b4910a161a57b686958f6aa711b16316e10a76b3b8c15d2d76b76d5a6071ee2f2e761d85092f27a7170d6f063da5606bcd3c
-  passwd_hash_algo: pbkdf2$320000$50
+  passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889
+  passwd_hash_algo: pbkdf2$50000$50
   must_change_password: false
   login_source: 0
   login_name: user24
@@ -896,8 +896,8 @@
   email: org25@example.com
   keep_email_private: false
   email_notifications_preference: enabled
-  passwd: b4910a161a57b686958f6aa711b16316e10a76b3b8c15d2d76b76d5a6071ee2f2e761d85092f27a7170d6f063da5606bcd3c
-  passwd_hash_algo: pbkdf2$320000$50
+  passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889
+  passwd_hash_algo: pbkdf2$50000$50
   must_change_password: false
   login_source: 0
   login_name: org25
@@ -933,8 +933,8 @@
   email: org26@example.com
   keep_email_private: false
   email_notifications_preference: onmention
-  passwd: b4910a161a57b686958f6aa711b16316e10a76b3b8c15d2d76b76d5a6071ee2f2e761d85092f27a7170d6f063da5606bcd3c
-  passwd_hash_algo: pbkdf2$320000$50
+  passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889
+  passwd_hash_algo: pbkdf2$50000$50
   must_change_password: false
   login_source: 0
   login_name: org26
@@ -970,8 +970,8 @@
   email: user27@example.com
   keep_email_private: false
   email_notifications_preference: enabled
-  passwd: b4910a161a57b686958f6aa711b16316e10a76b3b8c15d2d76b76d5a6071ee2f2e761d85092f27a7170d6f063da5606bcd3c
-  passwd_hash_algo: pbkdf2$320000$50
+  passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889
+  passwd_hash_algo: pbkdf2$50000$50
   must_change_password: false
   login_source: 0
   login_name: user27
@@ -1007,8 +1007,8 @@
   email: user28@example.com
   keep_email_private: true
   email_notifications_preference: enabled
-  passwd: b4910a161a57b686958f6aa711b16316e10a76b3b8c15d2d76b76d5a6071ee2f2e761d85092f27a7170d6f063da5606bcd3c
-  passwd_hash_algo: pbkdf2$320000$50
+  passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889
+  passwd_hash_algo: pbkdf2$50000$50
   must_change_password: false
   login_source: 0
   login_name: user28
@@ -1044,8 +1044,8 @@
   email: user29@example.com
   keep_email_private: false
   email_notifications_preference: enabled
-  passwd: b4910a161a57b686958f6aa711b16316e10a76b3b8c15d2d76b76d5a6071ee2f2e761d85092f27a7170d6f063da5606bcd3c
-  passwd_hash_algo: pbkdf2$320000$50
+  passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889
+  passwd_hash_algo: pbkdf2$50000$50
   must_change_password: false
   login_source: 0
   login_name: user29
@@ -1081,8 +1081,8 @@
   email: user30@example.com
   keep_email_private: false
   email_notifications_preference: enabled
-  passwd: b4910a161a57b686958f6aa711b16316e10a76b3b8c15d2d76b76d5a6071ee2f2e761d85092f27a7170d6f063da5606bcd3c
-  passwd_hash_algo: pbkdf2$320000$50
+  passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889
+  passwd_hash_algo: pbkdf2$50000$50
   must_change_password: false
   login_source: 0
   login_name: user30
@@ -1118,8 +1118,8 @@
   email: user31@example.com
   keep_email_private: false
   email_notifications_preference: enabled
-  passwd: b4910a161a57b686958f6aa711b16316e10a76b3b8c15d2d76b76d5a6071ee2f2e761d85092f27a7170d6f063da5606bcd3c
-  passwd_hash_algo: pbkdf2$320000$50
+  passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889
+  passwd_hash_algo: pbkdf2$50000$50
   must_change_password: false
   login_source: 0
   login_name: user31
@@ -1192,8 +1192,8 @@
   email: user33@example.com
   keep_email_private: false
   email_notifications_preference: enabled
-  passwd: b4910a161a57b686958f6aa711b16316e10a76b3b8c15d2d76b76d5a6071ee2f2e761d85092f27a7170d6f063da5606bcd3c
-  passwd_hash_algo: pbkdf2$320000$50
+  passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889
+  passwd_hash_algo: pbkdf2$50000$50
   must_change_password: false
   login_source: 0
   login_name: user33
diff --git a/modules/auth/password/hash/argon2.go b/modules/auth/password/hash/argon2.go
index 78bd5f88670f5..921625804431b 100644
--- a/modules/auth/password/hash/argon2.go
+++ b/modules/auth/password/hash/argon2.go
@@ -13,7 +13,7 @@ import (
 )
 
 func init() {
-	registerHasher("argon2", NewArgon2Hasher)
+	Register("argon2", NewArgon2Hasher)
 }
 
 // Argon2Hasher implements PasswordHasher
diff --git a/modules/auth/password/hash/bcrypt.go b/modules/auth/password/hash/bcrypt.go
index 86dfad71f23fd..3f21a7efd0405 100644
--- a/modules/auth/password/hash/bcrypt.go
+++ b/modules/auth/password/hash/bcrypt.go
@@ -8,7 +8,7 @@ import (
 )
 
 func init() {
-	registerHasher("bcrypt", NewBcryptHasher)
+	Register("bcrypt", NewBcryptHasher)
 }
 
 // BcryptHasher implements PasswordHasher
diff --git a/modules/auth/password/hash/hash.go b/modules/auth/password/hash/hash.go
index ab0350865c5a9..c2951e3ea7bb9 100644
--- a/modules/auth/password/hash/hash.go
+++ b/modules/auth/password/hash/hash.go
@@ -83,7 +83,9 @@ var (
 	availableHasherFactories = map[string]func(string) PasswordSaltHasher{}
 )
 
-func registerHasher[T PasswordSaltHasher](name string, newFn func(config string) T) {
+// Register registers a PasswordSaltHasher with the availableHasherFactories
+// 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))
 	}
@@ -122,9 +124,7 @@ func Parse(algorithm string) *PasswordHashAlgorithm {
 	if len(vals) == 0 {
 		return nil
 	}
-	if len(vals) > 0 {
-		name = vals[0]
-	}
+	name = vals[0]
 	if len(vals) > 1 {
 		config = vals[1]
 	}
diff --git a/modules/auth/password/hash/hash_test.go b/modules/auth/password/hash/hash_test.go
index 992e9690de352..c191cb1b08cc4 100644
--- a/modules/auth/password/hash/hash_test.go
+++ b/modules/auth/password/hash/hash_test.go
@@ -19,12 +19,12 @@ func (t testSaltHasher) HashWithSaltBytes(password string, salt []byte) string {
 }
 
 func Test_registerHasher(t *testing.T) {
-	registerHasher("Test_registerHasher", func(config string) testSaltHasher {
+	Register("Test_registerHasher", func(config string) testSaltHasher {
 		return testSaltHasher(config)
 	})
 
 	assert.Panics(t, func() {
-		registerHasher("Test_registerHasher", func(config string) testSaltHasher {
+		Register("Test_registerHasher", func(config string) testSaltHasher {
 			return testSaltHasher(config)
 		})
 	})
@@ -184,3 +184,8 @@ func TestVectors(t *testing.T) {
 		}
 	}
 }
+
+func TestPassword(t *testing.T) {
+	hash, err := Parse("pbkdf2$50000$50").Hash("password", "ZogKvWdyEx")
+	assert.Fail(t, hash, "%s %v", hash, err)
+}
diff --git a/modules/auth/password/hash/pbkdf2.go b/modules/auth/password/hash/pbkdf2.go
index 876bba4f27aff..b49d453b2a0ee 100644
--- a/modules/auth/password/hash/pbkdf2.go
+++ b/modules/auth/password/hash/pbkdf2.go
@@ -14,7 +14,7 @@ import (
 )
 
 func init() {
-	registerHasher("pbkdf2", NewPBKDF2Hasher)
+	Register("pbkdf2", NewPBKDF2Hasher)
 }
 
 // PBKDF2Hasher implements PasswordHasher
diff --git a/modules/auth/password/hash/scrypt.go b/modules/auth/password/hash/scrypt.go
index e467d119bfbac..392db86270ae2 100644
--- a/modules/auth/password/hash/scrypt.go
+++ b/modules/auth/password/hash/scrypt.go
@@ -13,7 +13,7 @@ import (
 )
 
 func init() {
-	registerHasher("scrypt", NewScryptHasher)
+	Register("scrypt", NewScryptHasher)
 }
 
 // ScryptHasher implements PasswordHasher
diff --git a/modules/auth/password/hash/setting.go b/modules/auth/password/hash/setting.go
index fcbce0aa3135c..22f97dffe565d 100644
--- a/modules/auth/password/hash/setting.go
+++ b/modules/auth/password/hash/setting.go
@@ -15,7 +15,10 @@ var aliasAlgorithmNames = map[string]string{
 	"pbkdf2_v1": "pbkdf2$10000$50",
 	// The latest PBKDF2 password algorithm is used as the default since it doesn't
 	// use a lot of  memory and is safer to use on less powerful devices.
-	"pbkdf2_v2": "pbkdf2$320000$50",
+	"pbkdf2_v2": "pbkdf2$50000$50",
+	// The pbkdf2_hi password algorithm is offered as a stronger alternative to the
+	// slightly improved pbkdf2_v2 algorithm
+	"pbkdf2_hi": "pbkdf2$320000$50",
 }
 
 var RecommendedHashAlgorithms = []string{
@@ -23,6 +26,7 @@ var RecommendedHashAlgorithms = []string{
 	"argon2",
 	"bcrypt",
 	"scrypt",
+	"pbkdf2_hi",
 }
 
 func SetDefaultPasswordHashAlgorithm(algorithmName string) (string, *PasswordHashAlgorithm) {

From 17f9402be61cc7d9b37d66c66f9790be99af48ff Mon Sep 17 00:00:00 2001
From: Andrew Thornton <art27@cantab.net>
Date: Sat, 18 Feb 2023 12:49:41 +0000
Subject: [PATCH 07/10] oops

Signed-off-by: Andrew Thornton <art27@cantab.net>
---
 modules/auth/password/hash/hash_test.go | 5 -----
 1 file changed, 5 deletions(-)

diff --git a/modules/auth/password/hash/hash_test.go b/modules/auth/password/hash/hash_test.go
index c191cb1b08cc4..593c8386a3558 100644
--- a/modules/auth/password/hash/hash_test.go
+++ b/modules/auth/password/hash/hash_test.go
@@ -184,8 +184,3 @@ func TestVectors(t *testing.T) {
 		}
 	}
 }
-
-func TestPassword(t *testing.T) {
-	hash, err := Parse("pbkdf2$50000$50").Hash("password", "ZogKvWdyEx")
-	assert.Fail(t, hash, "%s %v", hash, err)
-}

From 66c95c5ddb5f2a3a5cd80192855e9ddec65fb32e Mon Sep 17 00:00:00 2001
From: Andrew Thornton <art27@cantab.net>
Date: Sat, 18 Feb 2023 14:00:44 +0000
Subject: [PATCH 08/10] More comments to be more explicit

Signed-off-by: Andrew Thornton <art27@cantab.net>
---
 modules/auth/password/hash/argon2.go       |  3 +
 modules/auth/password/hash/bcrypt.go       |  3 +
 modules/auth/password/hash/hash.go         | 73 ++++++++++++++++------
 modules/auth/password/hash/pbkdf2.go       |  5 ++
 modules/auth/password/hash/scrypt.go       |  3 +
 modules/auth/password/hash/setting.go      | 17 +++++
 modules/auth/password/hash/setting_test.go |  6 +-
 7 files changed, 87 insertions(+), 23 deletions(-)

diff --git a/modules/auth/password/hash/argon2.go b/modules/auth/password/hash/argon2.go
index 921625804431b..14c16b53c4287 100644
--- a/modules/auth/password/hash/argon2.go
+++ b/modules/auth/password/hash/argon2.go
@@ -41,6 +41,9 @@ func NewArgon2Hasher(config string) *Argon2Hasher {
 	// This default configuration uses the following parameters:
 	// time=2, memory=64*1024, threads=8, keyLen=50.
 	// It will make two passes through the memory, using 64MiB in total.
+	// This matches the original configuration for `argon2` prior to storing hash parameters
+	// in the database.
+	// THESE VALUES MUST NOT BE CHANGED OR BACKWARDS COMPATIBILITY WILL BREAK
 	hasher := &Argon2Hasher{
 		time:    2,
 		memory:  1 << 16,
diff --git a/modules/auth/password/hash/bcrypt.go b/modules/auth/password/hash/bcrypt.go
index 3f21a7efd0405..ddf5420408f30 100644
--- a/modules/auth/password/hash/bcrypt.go
+++ b/modules/auth/password/hash/bcrypt.go
@@ -34,6 +34,9 @@ func (hasher *BcryptHasher) VerifyPassword(password, hashedPassword, salt string
 // The provided config should be either empty or the string representation of the "<cost>"
 // as an integer
 func NewBcryptHasher(config string) *BcryptHasher {
+	// This matches the original configuration for `bcrypt` prior to storing hash parameters
+	// in the database.
+	// THESE VALUES MUST NOT BE CHANGED OR BACKWARDS COMPATIBILITY WILL BREAK
 	hasher := &BcryptHasher{
 		cost: 10, // cost=10. i.e. 2^10 rounds of key expansion.
 	}
diff --git a/modules/auth/password/hash/hash.go b/modules/auth/password/hash/hash.go
index c2951e3ea7bb9..3572dd46d4777 100644
--- a/modules/auth/password/hash/hash.go
+++ b/modules/auth/password/hash/hash.go
@@ -35,7 +35,7 @@ type PasswordVerifier interface {
 // PasswordHashAlgorithms are named PasswordSaltHashers with a default verifier and hash function
 type PasswordHashAlgorithm struct {
 	PasswordSaltHasher
-	Name string
+	Specification string // The specification that is used to create the internal PasswordSaltHasher
 }
 
 // Hash the provided password with the salt and return the hash
@@ -61,8 +61,8 @@ func (algorithm *PasswordHashAlgorithm) Hash(password, salt string) (string, err
 
 // Verify the provided password matches the hashPassword when hashed with the salt
 func (algorithm *PasswordHashAlgorithm) VerifyPassword(providedPassword, hashedPassword, salt string) bool {
-	// The bcrypt package has its own specialized compare function that takes into
-	// account the stored password's bcrypt parameters.
+	// 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)
 	}
@@ -70,7 +70,7 @@ func (algorithm *PasswordHashAlgorithm) VerifyPassword(providedPassword, hashedP
 	// Compute the hash of the password.
 	providedPasswordHash, err := algorithm.Hash(providedPassword, salt)
 	if err != nil {
-		log.Error("passwordhash: %v.Hash(): %v", algorithm.Name, err)
+		log.Error("passwordhash: %v.Hash(): %v", algorithm.Specification, err)
 		return false
 	}
 
@@ -84,7 +84,7 @@ var (
 )
 
 // Register registers a PasswordSaltHasher with the availableHasherFactories
-// This is not thread safe.
+// 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))
@@ -96,49 +96,82 @@ func Register[T PasswordSaltHasher](name string, newFn func(config string) T) {
 	}
 }
 
-// In early versions of gitea the password hash algorithm field could be empty
-// At that point the default was `pbkdf2` without configuration values
-// Please note this is not the same as the DefaultAlgorithm
-const defaultEmptyHashAlgorithmName = "pbkdf2"
-
-func Parse(algorithm string) *PasswordHashAlgorithm {
-	if algorithm == "" {
-		algorithm = defaultEmptyHashAlgorithmName
+// 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 && algorithm == DefaultHashAlgorithm.Name {
+	if DefaultHashAlgorithm != nil && algorithmSpec == DefaultHashAlgorithm.Specification {
 		return DefaultHashAlgorithm
 	}
 
 	ptr := lastNonDefaultAlgorithm.Load()
 	if ptr != nil {
 		hashAlgorithm, ok := ptr.(*PasswordHashAlgorithm)
-		if ok && hashAlgorithm.Name == algorithm {
+		if ok && hashAlgorithm.Specification == algorithmSpec {
 			return hashAlgorithm
 		}
 	}
 
-	vals := strings.SplitN(algorithm, "$", 2)
-	var name string
+	// 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
 	}
-	name = vals[0]
+
+	hasherType = vals[0]
 	if len(vals) > 1 {
 		config = vals[1]
 	}
-	newFn, has := availableHasherFactories[name]
+
+	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,
-		Name:               algorithm,
+		Specification:      algorithmSpec,
 	}
 
 	lastNonDefaultAlgorithm.Store(hashAlgorithm)
diff --git a/modules/auth/password/hash/pbkdf2.go b/modules/auth/password/hash/pbkdf2.go
index b49d453b2a0ee..be3121318e3ea 100644
--- a/modules/auth/password/hash/pbkdf2.go
+++ b/modules/auth/password/hash/pbkdf2.go
@@ -36,6 +36,11 @@ func (hasher *PBKDF2Hasher) HashWithSaltBytes(password string, salt []byte) stri
 // "<iter>$<keyLen>", where <x> is the string representation
 // of an integer
 func NewPBKDF2Hasher(config string) *PBKDF2Hasher {
+	// This default configuration uses the following parameters:
+	// iter=10000, keyLen=50.
+	// This matches the original configuration for `pbkdf2` prior to storing parameters
+	// in the database.
+	// THESE VALUES MUST NOT BE CHANGED OR BACKWARDS COMPATIBILITY WILL BREAK
 	hasher := &PBKDF2Hasher{
 		iter:   10_000,
 		keyLen: 50,
diff --git a/modules/auth/password/hash/scrypt.go b/modules/auth/password/hash/scrypt.go
index 392db86270ae2..e77434fc32b2a 100644
--- a/modules/auth/password/hash/scrypt.go
+++ b/modules/auth/password/hash/scrypt.go
@@ -36,6 +36,9 @@ func (hasher *ScryptHasher) HashWithSaltBytes(password string, salt []byte) stri
 // "<n>$<r>$<p>$<keyLen>", where <x> is the string representation
 // of an integer
 func NewScryptHasher(config string) *ScryptHasher {
+	// This matches the original configuration for `scrypt` prior to storing hash parameters
+	// in the database.
+	// THESE VALUES MUST NOT BE CHANGED OR BACKWARDS COMPATIBILITY WILL BREAK
 	hasher := &ScryptHasher{
 		n:      1 << 16,
 		r:      16,
diff --git a/modules/auth/password/hash/setting.go b/modules/auth/password/hash/setting.go
index 22f97dffe565d..8e30eb95a59b2 100644
--- a/modules/auth/password/hash/setting.go
+++ b/modules/auth/password/hash/setting.go
@@ -3,10 +3,22 @@
 
 package hash
 
+// DefaultHashAlgorithmName represents the defualt value of PASSWORD_HASH_ALGO
+// configured in app.ini.
+//
+// It is NOT the same and does NOT map to the defaultEmptyHashAlgorithmSpecification.
+//
+// It will be dealiased as per aliasAlgorithmNames whereas
+// defaultEmptyHashAlgorithmSpecification does not undergo dealiasing.
 const DefaultHashAlgorithmName = "pbkdf2"
 
 var DefaultHashAlgorithm *PasswordHashAlgorithm
 
+// aliasAlgorithNames provides a mapping between the value of PASSWORD_HASH_ALGO
+// configured in the app.ini and the parameters used within the hashers internally.
+//
+// If it is necessary to change the default parameters for any hasher in future you
+// should change these values and not those in argon2.go etc.
 var aliasAlgorithmNames = map[string]string{
 	"argon2":    "argon2$2$65536$8$50",
 	"bcrypt":    "bcrypt$10",
@@ -29,6 +41,8 @@ var RecommendedHashAlgorithms = []string{
 	"pbkdf2_hi",
 }
 
+// SetDefaultPasswordHashAlgorithm will take a provided algorithmName and dealias it to
+// a complete algorithm specification.
 func SetDefaultPasswordHashAlgorithm(algorithmName string) (string, *PasswordHashAlgorithm) {
 	if algorithmName == "" {
 		algorithmName = DefaultHashAlgorithmName
@@ -38,6 +52,9 @@ func SetDefaultPasswordHashAlgorithm(algorithmName string) (string, *PasswordHas
 		algorithmName = alias
 		alias, has = aliasAlgorithmNames[algorithmName]
 	}
+
+	// algorithmName should now be a full algorithm specification
+	// e.g. pbkdf2$50000$50 rather than pbdkf2
 	DefaultHashAlgorithm = Parse(algorithmName)
 
 	return algorithmName, DefaultHashAlgorithm
diff --git a/modules/auth/password/hash/setting_test.go b/modules/auth/password/hash/setting_test.go
index 4c20ff179b3e5..d707207db6782 100644
--- a/modules/auth/password/hash/setting_test.go
+++ b/modules/auth/password/hash/setting_test.go
@@ -15,7 +15,7 @@ func TestCheckSettingPasswordHashAlgorithm(t *testing.T) {
 		pbkdf2Config, pbkdf2Algo := SetDefaultPasswordHashAlgorithm("pbkdf2")
 
 		assert.Equal(t, pbkdf2v2Config, pbkdf2Config)
-		assert.Equal(t, pbkdf2v2Algo.Name, pbkdf2Algo.Name)
+		assert.Equal(t, pbkdf2v2Algo.Specification, pbkdf2Algo.Specification)
 	})
 
 	for a, b := range aliasAlgorithmNames {
@@ -24,7 +24,7 @@ func TestCheckSettingPasswordHashAlgorithm(t *testing.T) {
 			bConfig, bAlgo := SetDefaultPasswordHashAlgorithm(b)
 
 			assert.Equal(t, bConfig, aConfig)
-			assert.Equal(t, aAlgo.Name, bAlgo.Name)
+			assert.Equal(t, aAlgo.Specification, bAlgo.Specification)
 		})
 	}
 
@@ -33,6 +33,6 @@ func TestCheckSettingPasswordHashAlgorithm(t *testing.T) {
 		pbkdf2v2Config, pbkdf2v2Algo := SetDefaultPasswordHashAlgorithm("pbkdf2_v2")
 
 		assert.Equal(t, pbkdf2v2Config, emptyConfig)
-		assert.Equal(t, pbkdf2v2Algo.Name, emptyAlgo.Name)
+		assert.Equal(t, pbkdf2v2Algo.Specification, emptyAlgo.Specification)
 	})
 }

From 0dcd2962235980148f8296637b44f1cf072424da Mon Sep 17 00:00:00 2001
From: Andrew Thornton <art27@cantab.net>
Date: Sat, 18 Feb 2023 14:13:03 +0000
Subject: [PATCH 09/10] placate linter

Signed-off-by: Andrew Thornton <art27@cantab.net>
---
 modules/auth/password/hash/setting.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/modules/auth/password/hash/setting.go b/modules/auth/password/hash/setting.go
index 8e30eb95a59b2..70169743040ab 100644
--- a/modules/auth/password/hash/setting.go
+++ b/modules/auth/password/hash/setting.go
@@ -3,7 +3,7 @@
 
 package hash
 
-// DefaultHashAlgorithmName represents the defualt value of PASSWORD_HASH_ALGO
+// DefaultHashAlgorithmName represents the default value of PASSWORD_HASH_ALGO
 // configured in app.ini.
 //
 // It is NOT the same and does NOT map to the defaultEmptyHashAlgorithmSpecification.

From 3e43075f1fb5832b204212e1ce21b921ecd67ce7 Mon Sep 17 00:00:00 2001
From: Andrew Thornton <art27@cantab.net>
Date: Sat, 18 Feb 2023 14:27:59 +0000
Subject: [PATCH 10/10] add pbkdf2_hi to docs

Signed-off-by: Andrew Thornton <art27@cantab.net>
---
 docs/content/doc/advanced/config-cheat-sheet.en-us.md | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md
index aa0481f8b4249..36e9919bc7097 100644
--- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md
+++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md
@@ -568,7 +568,7 @@ Certain queues have defaults that override the defaults set in `[queue]` (this o
 - `IMPORT_LOCAL_PATHS`: **false**: Set to `false` to prevent all users (including admin) from importing local path on server.
 - `INTERNAL_TOKEN`: **\<random at every install if no uri set\>**: Secret used to validate communication within Gitea binary.
 - `INTERNAL_TOKEN_URI`: **<empty>**: Instead of defining INTERNAL_TOKEN in the configuration, this configuration option can be used to give Gitea a path to a file that contains the internal token (example value: `file:/etc/gitea/internal_token`)
-- `PASSWORD_HASH_ALGO`: **pbkdf2**: The hash algorithm to use \[argon2, pbkdf2, pbkdf2_v1, scrypt, bcrypt\], argon2 and scrypt will spend significant amounts of memory.
+- `PASSWORD_HASH_ALGO`: **pbkdf2**: The hash algorithm to use \[argon2, pbkdf2, pbkdf2_v1, pbkdf2_hi, scrypt, bcrypt\], argon2 and scrypt will spend significant amounts of memory.
   - Note: The default parameters for `pbkdf2` hashing have changed - the previous settings are available as `pbkdf2_v1` but are not recommended.
   - The hash functions may be tuned by using `$` after the algorithm:
     - `argon2$<time>$<memory>$<threads>$<key-length>`
@@ -578,9 +578,10 @@ Certain queues have defaults that override the defaults set in `[queue]` (this o
   - The defaults are:
     - `argon2`:    `argon2$2$65536$8$50`
     - `bcrypt`:    `bcrypt$10`
-    - `pbkdf2`:    `pbkdf2$320000$50`
+    - `pbkdf2`:    `pbkdf2$50000$50`
     - `pbkdf2_v1`: `pbkdf2$10000$50`
-    - `pbkdf2_v2`: `pbkdf2$320000$50`
+    - `pbkdf2_v2`: `pbkdf2$50000$50`
+    - `pbkdf2_hi`: `pbkdf2$320000$50`
     - `scrypt`:    `scrypt$65536$16$2$50`
   - Adjusting the algorithm parameters using this functionality is done at your own risk.
 - `CSRF_COOKIE_HTTP_ONLY`: **true**: Set false to allow JavaScript to read CSRF cookie.