aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--cmd/admin_user_change_password.go2
-rw-r--r--cmd/admin_user_create.go2
-rw-r--r--docs/content/doc/advanced/config-cheat-sheet.en-us.md16
-rw-r--r--models/fixtures/user.yml130
-rw-r--r--models/user/user.go75
-rw-r--r--models/user/user_test.go3
-rw-r--r--modules/auth/password/hash/argon2.go77
-rw-r--r--modules/auth/password/hash/bcrypt.go51
-rw-r--r--modules/auth/password/hash/common.go28
-rw-r--r--modules/auth/password/hash/hash.go147
-rw-r--r--modules/auth/password/hash/hash_test.go186
-rw-r--r--modules/auth/password/hash/pbkdf2.go62
-rw-r--r--modules/auth/password/hash/scrypt.go64
-rw-r--r--modules/auth/password/hash/setting.go44
-rw-r--r--modules/auth/password/hash/setting_test.go38
-rw-r--r--modules/auth/password/password.go (renamed from modules/password/password.go)8
-rw-r--r--modules/auth/password/password_test.go (renamed from modules/password/password_test.go)0
-rw-r--r--modules/auth/password/pwn.go (renamed from modules/password/pwn.go)0
-rw-r--r--modules/setting/setting.go10
-rw-r--r--routers/api/v1/admin/user.go2
-rw-r--r--routers/install/install.go3
-rw-r--r--routers/web/admin/users.go2
-rw-r--r--routers/web/auth/auth.go2
-rw-r--r--routers/web/auth/password.go2
-rw-r--r--routers/web/user/setting/account.go2
25 files changed, 805 insertions, 151 deletions
diff --git a/cmd/admin_user_change_password.go b/cmd/admin_user_change_password.go
index 1b7c73370d..7866bde912 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 579c6f2f62..09eaad54be 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 bb1ae7b4ab..26cacf408a 100644
--- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md
+++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md
@@ -523,7 +523,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>`
+ - `bcrypt$<cost>`
+ - `pbkdf2$<iterations>$<key-length>`
+ - `scrypt$<n>$<r>$<p>$<key-length>`
+ - The defaults are:
+ - `argon2`: `argon2$2$65536$8$50`
+ - `bcrypt`: `bcrypt$10`
+ - `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.
- `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/fixtures/user.yml b/models/fixtures/user.yml
index 36bc049ca4..bdc97bf538 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: 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: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
- passwd_hash_algo: argon2
+ 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: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
- passwd_hash_algo: argon2
+ 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: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
- passwd_hash_algo: argon2
+ 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: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
- passwd_hash_algo: argon2
+ 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: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
- passwd_hash_algo: argon2
+ 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: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
- passwd_hash_algo: argon2
+ 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: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
- passwd_hash_algo: argon2
+ 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: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
- passwd_hash_algo: argon2
+ 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: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
- passwd_hash_algo: argon2
+ 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: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
- passwd_hash_algo: argon2
+ 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: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
- passwd_hash_algo: argon2
+ 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: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
- passwd_hash_algo: argon2
+ 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: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
- passwd_hash_algo: argon2
+ 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: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
- passwd_hash_algo: argon2
+ 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: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
- passwd_hash_algo: argon2
+ 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: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
- passwd_hash_algo: argon2
+ 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: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
- passwd_hash_algo: argon2
+ 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: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
- passwd_hash_algo: argon2
+ 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: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
- passwd_hash_algo: argon2
+ 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: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
- passwd_hash_algo: argon2
+ 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: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
- passwd_hash_algo: argon2
+ 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: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
- passwd_hash_algo: argon2
+ 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: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
- passwd_hash_algo: argon2
+ 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: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
- passwd_hash_algo: argon2
+ 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: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
- passwd_hash_algo: argon2
+ 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: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
- passwd_hash_algo: argon2
+ 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: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
- passwd_hash_algo: argon2
+ 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: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
- passwd_hash_algo: argon2
+ 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: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
- passwd_hash_algo: argon2
+ 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: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
- passwd_hash_algo: argon2
+ passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889
+ passwd_hash_algo: pbkdf2$50000$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: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889
+ passwd_hash_algo: pbkdf2$50000$50
must_change_password: false
login_source: 0
login_name: user33
diff --git a/models/user/user.go b/models/user/user.go
index 63d9b8ad6b..7aa2635624 100644
--- a/models/user/user.go
+++ b/models/user/user.go
@@ -7,8 +7,6 @@ package user
import (
"context"
- "crypto/sha256"
- "crypto/subtle"
"encoding/hex"
"fmt"
"net/url"
@@ -22,6 +20,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/timeutil"
"code.gitea.io/gitea/modules/util"
- "golang.org/x/crypto/argon2"
- "golang.org/x/crypto/bcrypt"
- "golang.org/x/crypto/pbkdf2"
- "golang.org/x/crypto/scrypt"
"xorm.io/builder"
)
@@ -49,21 +44,6 @@ const (
)
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"
// EmailNotificationsOnMention indicates that the user would like to be notified via email when mentioned.
@@ -368,42 +348,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 fmt.Sprintf("%x", 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) {
@@ -417,7 +361,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
@@ -425,20 +369,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 5f2ac0a60c..37fa711d83 100644
--- a/models/user/user_test.go
+++ b/models/user/user_test.go
@@ -13,6 +13,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/util"
@@ -162,7 +163,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 0000000000..9216258044
--- /dev/null
+++ b/modules/auth/password/hash/argon2.go
@@ -0,0 +1,77 @@
+// 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() {
+ Register("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 0000000000..3f21a7efd0
--- /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() {
+ Register("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 0000000000..ac6faf35cf
--- /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 0000000000..c2951e3ea7
--- /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{}
+)
+
+// 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))
+ }
+
+ 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
+ }
+ 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 0000000000..593c8386a3
--- /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) {
+ Register("Test_registerHasher", func(config string) testSaltHasher {
+ return testSaltHasher(config)
+ })
+
+ assert.Panics(t, func() {
+ 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 {
+ 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 0000000000..b49d453b2a
--- /dev/null
+++ b/modules/auth/password/hash/pbkdf2.go
@@ -0,0 +1,62 @@
+// 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() {
+ Register("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 0000000000..392db86270
--- /dev/null
+++ b/modules/auth/password/hash/scrypt.go
@@ -0,0 +1,64 @@
+// 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() {
+ Register("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 0000000000..22f97dffe5
--- /dev/null
+++ b/modules/auth/password/hash/setting.go
@@ -0,0 +1,44 @@
+// 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$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",
+}
+
+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 0000000000..4c20ff179b
--- /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
index e1f1f769ec..2bad389f76 100644
--- a/modules/password/password.go
+++ b/modules/auth/password/password.go
@@ -12,8 +12,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
@@ -113,13 +113,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
index 63f98aa9c3..63f98aa9c3 100644
--- a/modules/password/password_test.go
+++ b/modules/auth/password/password_test.go
diff --git a/modules/password/pwn.go b/modules/auth/password/pwn.go
index 938524e6de..938524e6de 100644
--- a/modules/password/pwn.go
+++ b/modules/auth/password/pwn.go
diff --git a/modules/setting/setting.go b/modules/setting/setting.go
index 26a55c913b..8217d07722 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"
@@ -964,7 +965,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 1a4b020011..3faca09404 100644
--- a/routers/api/v1/admin/user.go
+++ b/routers/api/v1/admin/user.go
@@ -16,10 +16,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/context"
"code.gitea.io/gitea/modules/convert"
"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/util"
diff --git a/routers/install/install.go b/routers/install/install.go
index d5a40c859c..25b272b854 100644
--- a/routers/install/install.go
+++ b/routers/install/install.go
@@ -21,6 +21,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"
@@ -80,7 +81,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 5cdfb8142e..ef3645e478 100644
--- a/routers/web/admin/users.go
+++ b/routers/web/admin/users.go
@@ -15,10 +15,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 0f8128946c..dad297925e 100644
--- a/routers/web/auth/auth.go
+++ b/routers/web/auth/auth.go
@@ -14,13 +14,13 @@ 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/hcaptcha"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/mcaptcha"
- "code.gitea.io/gitea/modules/password"
"code.gitea.io/gitea/modules/recaptcha"
"code.gitea.io/gitea/modules/session"
"code.gitea.io/gitea/modules/setting"
diff --git a/routers/web/auth/password.go b/routers/web/auth/password.go
index c21ca9cf69..c5a41d24fe 100644
--- a/routers/web/auth/password.go
+++ b/routers/web/auth/password.go
@@ -10,10 +10,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 8b95caf2fc..552b1119bd 100644
--- a/routers/web/user/setting/account.go
+++ b/routers/web/user/setting/account.go
@@ -12,10 +12,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"