summaryrefslogtreecommitdiffstats
path: root/modules/auth/password/hash
diff options
context:
space:
mode:
Diffstat (limited to 'modules/auth/password/hash')
-rw-r--r--modules/auth/password/hash/argon2.go80
-rw-r--r--modules/auth/password/hash/bcrypt.go54
-rw-r--r--modules/auth/password/hash/common.go28
-rw-r--r--modules/auth/password/hash/dummy.go33
-rw-r--r--modules/auth/password/hash/dummy_test.go26
-rw-r--r--modules/auth/password/hash/hash.go189
-rw-r--r--modules/auth/password/hash/hash_test.go191
-rw-r--r--modules/auth/password/hash/pbkdf2.go67
-rw-r--r--modules/auth/password/hash/scrypt.go67
-rw-r--r--modules/auth/password/hash/setting.go76
-rw-r--r--modules/auth/password/hash/setting_test.go38
11 files changed, 849 insertions, 0 deletions
diff --git a/modules/auth/password/hash/argon2.go b/modules/auth/password/hash/argon2.go
new file mode 100644
index 0000000..0cd6472
--- /dev/null
+++ b/modules/auth/password/hash/argon2.go
@@ -0,0 +1,80 @@
+// 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() {
+ MustRegister("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.
+ // 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,
+ 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 0000000..4607c16
--- /dev/null
+++ b/modules/auth/password/hash/bcrypt.go
@@ -0,0 +1,54 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package hash
+
+import (
+ "golang.org/x/crypto/bcrypt"
+)
+
+func init() {
+ MustRegister("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 {
+ // 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.
+ }
+
+ 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 0000000..487c073
--- /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) { //nolint:unparam
+ 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/dummy.go b/modules/auth/password/hash/dummy.go
new file mode 100644
index 0000000..22f2e2f
--- /dev/null
+++ b/modules/auth/password/hash/dummy.go
@@ -0,0 +1,33 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package hash
+
+import (
+ "encoding/hex"
+)
+
+// DummyHasher implements PasswordHasher and is a dummy hasher that simply
+// puts the password in place with its salt
+// This SHOULD NOT be used in production and is provided to make the integration
+// tests faster only
+type DummyHasher struct{}
+
+// HashWithSaltBytes a provided password and salt
+func (hasher *DummyHasher) HashWithSaltBytes(password string, salt []byte) string {
+ if hasher == nil {
+ return ""
+ }
+
+ if len(salt) == 10 {
+ return string(salt) + ":" + password
+ }
+
+ return hex.EncodeToString(salt) + ":" + password
+}
+
+// NewDummyHasher is a factory method to create a DummyHasher
+// Any provided configuration is ignored
+func NewDummyHasher(_ string) *DummyHasher {
+ return &DummyHasher{}
+}
diff --git a/modules/auth/password/hash/dummy_test.go b/modules/auth/password/hash/dummy_test.go
new file mode 100644
index 0000000..35d1249
--- /dev/null
+++ b/modules/auth/password/hash/dummy_test.go
@@ -0,0 +1,26 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package hash
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestDummyHasher(t *testing.T) {
+ dummy := &PasswordHashAlgorithm{
+ PasswordSaltHasher: NewDummyHasher(""),
+ Specification: "dummy",
+ }
+
+ password, salt := "password", "ZogKvWdyEx"
+
+ hash, err := dummy.Hash(password, salt)
+ require.NoError(t, err)
+ assert.Equal(t, hash, salt+":"+password)
+
+ assert.True(t, dummy.VerifyPassword(password, hash, salt))
+}
diff --git a/modules/auth/password/hash/hash.go b/modules/auth/password/hash/hash.go
new file mode 100644
index 0000000..459320e
--- /dev/null
+++ b/modules/auth/password/hash/hash.go
@@ -0,0 +1,189 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package hash
+
+import (
+ "crypto/subtle"
+ "encoding/hex"
+ "fmt"
+ "strings"
+ "sync/atomic"
+
+ "code.gitea.io/gitea/modules/log"
+)
+
+// This package takes care of hashing passwords, verifying passwords, defining
+// available password algorithms, defining recommended password algorithms and
+// choosing the default password algorithm.
+
+// PasswordSaltHasher will hash a provided password with the provided saltBytes
+type PasswordSaltHasher interface {
+ HashWithSaltBytes(password string, saltBytes []byte) string
+}
+
+// PasswordHasher will hash a provided password with the salt
+type PasswordHasher interface {
+ Hash(password, salt string) (string, error)
+}
+
+// PasswordVerifier will ensure that a providedPassword matches the hashPassword when hashed with the salt
+type PasswordVerifier interface {
+ VerifyPassword(providedPassword, hashedPassword, salt string) bool
+}
+
+// PasswordHashAlgorithms are named PasswordSaltHashers with a default verifier and hash function
+type PasswordHashAlgorithm struct {
+ PasswordSaltHasher
+ Specification string // The specification that is used to create the internal PasswordSaltHasher
+}
+
+// Hash the provided password with the salt and return the hash
+func (algorithm *PasswordHashAlgorithm) Hash(password, salt string) (string, error) {
+ var saltBytes []byte
+
+ // There are two formats for the salt value:
+ // * The new format is a (32+)-byte hex-encoded string
+ // * The old format was a 10-byte binary format
+ // We have to tolerate both here.
+ if len(salt) == 10 {
+ saltBytes = []byte(salt)
+ } else {
+ var err error
+ saltBytes, err = hex.DecodeString(salt)
+ if err != nil {
+ return "", err
+ }
+ }
+
+ return algorithm.HashWithSaltBytes(password, saltBytes), nil
+}
+
+// Verify the provided password matches the hashPassword when hashed with the salt
+func (algorithm *PasswordHashAlgorithm) VerifyPassword(providedPassword, hashedPassword, salt string) bool {
+ // Some PasswordSaltHashers have their own specialised compare function that takes into
+ // account the stored parameters within the hash. e.g. bcrypt
+ if verifier, ok := algorithm.PasswordSaltHasher.(PasswordVerifier); ok {
+ return verifier.VerifyPassword(providedPassword, hashedPassword, salt)
+ }
+
+ // Compute the hash of the password.
+ providedPasswordHash, err := algorithm.Hash(providedPassword, salt)
+ if err != nil {
+ log.Error("passwordhash: %v.Hash(): %v", algorithm.Specification, err)
+ return false
+ }
+
+ // Compare it against the hashed password in constant-time.
+ return subtle.ConstantTimeCompare([]byte(hashedPassword), []byte(providedPasswordHash)) == 1
+}
+
+var (
+ lastNonDefaultAlgorithm atomic.Value
+ availableHasherFactories = map[string]func(string) PasswordSaltHasher{}
+)
+
+// MustRegister registers a PasswordSaltHasher with the availableHasherFactories
+// Caution: This is not thread safe.
+func MustRegister[T PasswordSaltHasher](name string, newFn func(config string) T) {
+ if err := Register(name, newFn); err != nil {
+ panic(err)
+ }
+}
+
+// Register registers a PasswordSaltHasher with the availableHasherFactories
+// Caution: This is not thread safe.
+func Register[T PasswordSaltHasher](name string, newFn func(config string) T) error {
+ if _, has := availableHasherFactories[name]; has {
+ return fmt.Errorf("duplicate registration of password salt hasher: %s", name)
+ }
+
+ availableHasherFactories[name] = func(config string) PasswordSaltHasher {
+ n := newFn(config)
+ return n
+ }
+ return nil
+}
+
+// In early versions of gitea the password hash algorithm field of a user could be
+// empty. At that point the default was `pbkdf2` without configuration values
+//
+// Please note this is not the same as the DefaultAlgorithm which is used
+// to determine what an empty PASSWORD_HASH_ALGO setting in the app.ini means.
+// These are not the same even if they have the same apparent value and they mean different things.
+//
+// DO NOT COALESCE THESE VALUES
+const defaultEmptyHashAlgorithmSpecification = "pbkdf2"
+
+// Parse will convert the provided algorithm specification in to a PasswordHashAlgorithm
+// If the provided specification matches the DefaultHashAlgorithm Specification it will be
+// used.
+// In addition the last non-default hasher will be cached to help reduce the load from
+// parsing specifications.
+//
+// NOTE: No de-aliasing is done in this function, thus any specification which does not
+// contain a configuration will use the default values for that hasher. These are not
+// necessarily the same values as those obtained by dealiasing. This allows for
+// seamless backwards compatibility with the original configuration.
+//
+// To further labour this point, running `Parse("pbkdf2")` does not obtain the
+// same algorithm as setting `PASSWORD_HASH_ALGO=pbkdf2` in app.ini, nor is it intended to.
+// A user that has `password_hash_algo='pbkdf2'` in the db means get the original, unconfigured algorithm
+// Users will be migrated automatically as they log-in to have the complete specification stored
+// in their `password_hash_algo` fields by other code.
+func Parse(algorithmSpec string) *PasswordHashAlgorithm {
+ if algorithmSpec == "" {
+ algorithmSpec = defaultEmptyHashAlgorithmSpecification
+ }
+
+ if DefaultHashAlgorithm != nil && algorithmSpec == DefaultHashAlgorithm.Specification {
+ return DefaultHashAlgorithm
+ }
+
+ ptr := lastNonDefaultAlgorithm.Load()
+ if ptr != nil {
+ hashAlgorithm, ok := ptr.(*PasswordHashAlgorithm)
+ if ok && hashAlgorithm.Specification == algorithmSpec {
+ return hashAlgorithm
+ }
+ }
+
+ // Now convert the provided specification in to a hasherType +/- some configuration parameters
+ vals := strings.SplitN(algorithmSpec, "$", 2)
+ var hasherType string
+ var config string
+
+ if len(vals) == 0 {
+ // This should not happen as algorithmSpec should not be empty
+ // due to it being assigned to defaultEmptyHashAlgorithmSpecification above
+ // but we should be absolutely cautious here
+ return nil
+ }
+
+ hasherType = vals[0]
+ if len(vals) > 1 {
+ config = vals[1]
+ }
+
+ newFn, has := availableHasherFactories[hasherType]
+ if !has {
+ // unknown hasher type
+ return nil
+ }
+
+ ph := newFn(config)
+ if ph == nil {
+ // The provided configuration is likely invalid - it will have been logged already
+ // but we cannot hash safely
+ return nil
+ }
+
+ hashAlgorithm := &PasswordHashAlgorithm{
+ PasswordSaltHasher: ph,
+ Specification: algorithmSpec,
+ }
+
+ lastNonDefaultAlgorithm.Store(hashAlgorithm)
+
+ return hashAlgorithm
+}
diff --git a/modules/auth/password/hash/hash_test.go b/modules/auth/password/hash/hash_test.go
new file mode 100644
index 0000000..03d08a8
--- /dev/null
+++ b/modules/auth/password/hash/hash_test.go
@@ -0,0 +1,191 @@
+// 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"
+ "github.com/stretchr/testify/require"
+)
+
+type testSaltHasher string
+
+func (t testSaltHasher) HashWithSaltBytes(password string, salt []byte) string {
+ return password + "$" + string(salt) + "$" + string(t)
+}
+
+func Test_registerHasher(t *testing.T) {
+ MustRegister("Test_registerHasher", func(config string) testSaltHasher {
+ return testSaltHasher(config)
+ })
+
+ assert.Panics(t, func() {
+ MustRegister("Test_registerHasher", func(config string) testSaltHasher {
+ return testSaltHasher(config)
+ })
+ })
+
+ require.Error(t, Register("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 {
+ require.NoError(t, err)
+ assert.NotEmpty(t, output, "output for %s was empty", algorithmName)
+ } else {
+ require.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 0000000..27382fe
--- /dev/null
+++ b/modules/auth/password/hash/pbkdf2.go
@@ -0,0 +1,67 @@
+// 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() {
+ MustRegister("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 {
+ // 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,
+ }
+
+ 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 0000000..f3d38f7
--- /dev/null
+++ b/modules/auth/password/hash/scrypt.go
@@ -0,0 +1,67 @@
+// 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() {
+ MustRegister("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 {
+ // 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,
+ 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 0000000..05cd36f
--- /dev/null
+++ b/modules/auth/password/hash/setting.go
@@ -0,0 +1,76 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package hash
+
+// 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.
+//
+// It will be dealiased as per aliasAlgorithmNames whereas
+// defaultEmptyHashAlgorithmSpecification does not undergo dealiasing.
+const DefaultHashAlgorithmName = "pbkdf2_hi"
+
+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",
+ "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$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{
+ "pbkdf2",
+ "argon2",
+ "bcrypt",
+ "scrypt",
+ "pbkdf2_hi",
+}
+
+// hashAlgorithmToSpec converts an algorithm name or a specification to a full algorithm specification
+func hashAlgorithmToSpec(algorithmName string) string {
+ if algorithmName == "" {
+ algorithmName = DefaultHashAlgorithmName
+ }
+ alias, has := aliasAlgorithmNames[algorithmName]
+ for has {
+ algorithmName = alias
+ alias, has = aliasAlgorithmNames[algorithmName]
+ }
+ return algorithmName
+}
+
+// SetDefaultPasswordHashAlgorithm will take a provided algorithmName and de-alias it to
+// a complete algorithm specification.
+func SetDefaultPasswordHashAlgorithm(algorithmName string) (string, *PasswordHashAlgorithm) {
+ algoSpec := hashAlgorithmToSpec(algorithmName)
+ // now we get a full specification, e.g. pbkdf2$50000$50 rather than pbdkf2
+ DefaultHashAlgorithm = Parse(algoSpec)
+ return algoSpec, DefaultHashAlgorithm
+}
+
+// ConfigHashAlgorithm will try to find a "recommended algorithm name" defined by RecommendedHashAlgorithms for config
+// This function is not fast and is only used for the installation page
+func ConfigHashAlgorithm(algorithm string) string {
+ algorithm = hashAlgorithmToSpec(algorithm)
+ for _, recommAlgo := range RecommendedHashAlgorithms {
+ if algorithm == hashAlgorithmToSpec(recommAlgo) {
+ return recommAlgo
+ }
+ }
+ return algorithm
+}
diff --git a/modules/auth/password/hash/setting_test.go b/modules/auth/password/hash/setting_test.go
new file mode 100644
index 0000000..548d87c
--- /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.Specification, pbkdf2Algo.Specification)
+ })
+
+ 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.Specification, bAlgo.Specification)
+ })
+ }
+
+ t.Run("pbkdf2_hi is the default when default password hash algorithm is empty", func(t *testing.T) {
+ emptyConfig, emptyAlgo := SetDefaultPasswordHashAlgorithm("")
+ pbkdf2hiConfig, pbkdf2hiAlgo := SetDefaultPasswordHashAlgorithm("pbkdf2_hi")
+
+ assert.Equal(t, pbkdf2hiConfig, emptyConfig)
+ assert.Equal(t, pbkdf2hiAlgo.Specification, emptyAlgo.Specification)
+ })
+}