Provide the ability to set password hash algorithm parameters (#22942)

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>
Co-authored-by: delvh <dev.lh@web.de>
Co-authored-by: John Olheiser <john.olheiser@gmail.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
pull/22970/head^2
zeripath 2 years ago committed by GitHub
parent d5e417a33d
commit 61b89747ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      cmd/admin_user_change_password.go
  2. 2
      cmd/admin_user_create.go
  3. 17
      docs/content/doc/advanced/config-cheat-sheet.en-us.md
  4. 130
      models/fixtures/user.yml
  5. 75
      models/user/user.go
  6. 3
      models/user/user_test.go
  7. 80
      modules/auth/password/hash/argon2.go
  8. 54
      modules/auth/password/hash/bcrypt.go
  9. 28
      modules/auth/password/hash/common.go
  10. 180
      modules/auth/password/hash/hash.go
  11. 186
      modules/auth/password/hash/hash_test.go
  12. 67
      modules/auth/password/hash/pbkdf2.go
  13. 67
      modules/auth/password/hash/scrypt.go
  14. 61
      modules/auth/password/hash/setting.go
  15. 38
      modules/auth/password/hash/setting_test.go
  16. 8
      modules/auth/password/password.go
  17. 0
      modules/auth/password/password_test.go
  18. 2
      modules/auth/password/pwn.go
  19. 0
      modules/auth/password/pwn/pwn.go
  20. 0
      modules/auth/password/pwn/pwn_test.go
  21. 10
      modules/setting/setting.go
  22. 2
      routers/api/v1/admin/user.go
  23. 3
      routers/install/install.go
  24. 2
      routers/web/admin/users.go
  25. 2
      routers/web/auth/auth.go
  26. 2
      routers/web/auth/password.go
  27. 2
      routers/web/user/setting/account.go

@ -9,7 +9,7 @@ import (
"fmt" "fmt"
user_model "code.gitea.io/gitea/models/user" 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/setting"
"github.com/urfave/cli" "github.com/urfave/cli"

@ -10,7 +10,7 @@ import (
auth_model "code.gitea.io/gitea/models/auth" auth_model "code.gitea.io/gitea/models/auth"
user_model "code.gitea.io/gitea/models/user" 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/setting"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"

@ -568,7 +568,22 @@ 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. - `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`: **\<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`) - `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, 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>`
- `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$50000$50`
- `pbkdf2_v1`: `pbkdf2$10000$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. - `CSRF_COOKIE_HTTP_ONLY`: **true**: Set false to allow JavaScript to read CSRF cookie.
- `MIN_PASSWORD_LENGTH`: **6**: Minimum password length for new users. - `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): - `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):

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

@ -6,8 +6,6 @@ package user
import ( import (
"context" "context"
"crypto/sha256"
"crypto/subtle"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"net/url" "net/url"
@ -21,6 +19,7 @@ import (
"code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/auth/openid" "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/base"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
@ -30,10 +29,6 @@ import (
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/validation" "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" "xorm.io/builder"
) )
@ -48,21 +43,6 @@ const (
UserTypeOrganization 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 ( const (
// EmailNotificationsEnabled indicates that the user would like to receive all email notifications except your own // EmailNotificationsEnabled indicates that the user would like to receive all email notifications except your own
EmailNotificationsEnabled = "enabled" 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 // SetPassword hashes a password using the algorithm defined in the config value of PASSWORD_HASH_ALGO
// change passwd, salt and passwd_hash_algo fields // change passwd, salt and passwd_hash_algo fields
func (u *User) SetPassword(passwd string) (err error) { 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 { if u.Salt, err = GetUserSalt(); err != nil {
return err 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 return err
} }
u.PasswdHashAlgo = setting.PasswordHashAlgo u.PasswdHashAlgo = setting.PasswordHashAlgo
@ -434,20 +378,9 @@ func (u *User) SetPassword(passwd string) (err error) {
return nil 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 { func (u *User) ValidatePassword(passwd string) bool {
tempHash, err := hashPassword(passwd, u.Salt, u.PasswdHashAlgo) return hash.Parse(u.PasswdHashAlgo).VerifyPassword(passwd, u.Passwd, u.Salt)
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
} }
// IsPasswordSet checks if the password is set or left empty // IsPasswordSet checks if the password is set or left empty

@ -14,6 +14,7 @@ import (
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user" 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/setting"
"code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
@ -164,7 +165,7 @@ func TestEmailNotificationPreferences(t *testing.T) {
func TestHashPasswordDeterministic(t *testing.T) { func TestHashPasswordDeterministic(t *testing.T) {
b := make([]byte, 16) b := make([]byte, 16)
u := &user_model.User{} u := &user_model.User{}
algos := []string{"argon2", "pbkdf2", "scrypt", "bcrypt"} algos := hash.RecommendedHashAlgorithms
for j := 0; j < len(algos); j++ { for j := 0; j < len(algos); j++ {
u.PasswdHashAlgo = algos[j] u.PasswdHashAlgo = algos[j]
for i := 0; i < 50; i++ { for i := 0; i < 50; i++ {

@ -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() {
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.
// 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
}

@ -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() {
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 {
// 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
}

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

@ -0,0 +1,180 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package hash
import (
"crypto/subtle"
"encoding/hex"
"fmt"
"strings"
"sync/atomic"
"code.gitea.io/gitea/modules/log"
)
// This package takes care of hashing passwords, verifying passwords, defining
// available password algorithms, defining recommended password algorithms and
// choosing the default password algorithm.
// PasswordSaltHasher will hash a provided password with the provided saltBytes
type PasswordSaltHasher interface {
HashWithSaltBytes(password string, saltBytes []byte) string
}
// PasswordHasher will hash a provided password with the salt
type PasswordHasher interface {
Hash(password, salt string) (string, error)
}
// PasswordVerifier will ensure that a providedPassword matches the hashPassword when hashed with the salt
type PasswordVerifier interface {
VerifyPassword(providedPassword, hashedPassword, salt string) bool
}
// PasswordHashAlgorithms are named PasswordSaltHashers with a default verifier and hash function
type PasswordHashAlgorithm struct {
PasswordSaltHasher
Specification string // The specification that is used to create the internal PasswordSaltHasher
}
// Hash the provided password with the salt and return the hash
func (algorithm *PasswordHashAlgorithm) Hash(password, salt string) (string, error) {
var saltBytes []byte
// There are two formats for the salt value:
// * The new format is a (32+)-byte hex-encoded string
// * The old format was a 10-byte binary format
// We have to tolerate both here.
if len(salt) == 10 {
saltBytes = []byte(salt)
} else {
var err error
saltBytes, err = hex.DecodeString(salt)
if err != nil {
return "", err
}
}
return algorithm.HashWithSaltBytes(password, saltBytes), nil
}
// Verify the provided password matches the hashPassword when hashed with the salt
func (algorithm *PasswordHashAlgorithm) VerifyPassword(providedPassword, hashedPassword, salt string) bool {
// Some PasswordSaltHashers have their own specialised compare function that takes into
// account the stored parameters within the hash. e.g. bcrypt
if verifier, ok := algorithm.PasswordSaltHasher.(PasswordVerifier); ok {
return verifier.VerifyPassword(providedPassword, hashedPassword, salt)
}
// Compute the hash of the password.
providedPasswordHash, err := algorithm.Hash(providedPassword, salt)
if err != nil {
log.Error("passwordhash: %v.Hash(): %v", algorithm.Specification, err)
return false
}
// Compare it against the hashed password in constant-time.
return subtle.ConstantTimeCompare([]byte(hashedPassword), []byte(providedPasswordHash)) == 1
}
var (
lastNonDefaultAlgorithm atomic.Value
availableHasherFactories = map[string]func(string) PasswordSaltHasher{}
)
// Register registers a PasswordSaltHasher with the availableHasherFactories
// Caution: This is not thread safe.
func Register[T PasswordSaltHasher](name string, newFn func(config string) T) {
if _, has := availableHasherFactories[name]; has {
panic(fmt.Errorf("duplicate registration of password salt hasher: %s", name))
}
availableHasherFactories[name] = func(config string) PasswordSaltHasher {
n := newFn(config)
return n
}
}
// In early versions of gitea the password hash algorithm field of a user could be
// empty. At that point the default was `pbkdf2` without configuration values
//
// Please note this is not the same as the DefaultAlgorithm which is used
// to determine what an empty PASSWORD_HASH_ALGO setting in the app.ini means.
// These are not the same even if they have the same apparent value and they mean different things.
//
// DO NOT COALESCE THESE VALUES
const defaultEmptyHashAlgorithmSpecification = "pbkdf2"
// Parse will convert the provided algorithm specification in to a PasswordHashAlgorithm
// If the provided specification matches the DefaultHashAlgorithm Specification it will be
// used.
// In addition the last non-default hasher will be cached to help reduce the load from
// parsing specifications.
//
// NOTE: No de-aliasing is done in this function, thus any specification which does not
// contain a configuration will use the default values for that hasher. These are not
// necessarily the same values as those obtained by dealiasing. This allows for
// seamless backwards compatibility with the original configuration.
//
// To further labour this point, running `Parse("pbkdf2")` does not obtain the
// same algorithm as setting `PASSWORD_HASH_ALGO=pbkdf2` in app.ini, nor is it intended to.
// A user that has `password_hash_algo='pbkdf2'` in the db means get the original, unconfigured algorithm
// Users will be migrated automatically as they log-in to have the complete specification stored
// in their `password_hash_algo` fields by other code.
func Parse(algorithmSpec string) *PasswordHashAlgorithm {
if algorithmSpec == "" {
algorithmSpec = defaultEmptyHashAlgorithmSpecification
}
if DefaultHashAlgorithm != nil && algorithmSpec == DefaultHashAlgorithm.Specification {
return DefaultHashAlgorithm
}
ptr := lastNonDefaultAlgorithm.Load()
if ptr != nil {
hashAlgorithm, ok := ptr.(*PasswordHashAlgorithm)
if ok && hashAlgorithm.Specification == algorithmSpec {
return hashAlgorithm
}
}
// Now convert the provided specification in to a hasherType +/- some configuration parameters
vals := strings.SplitN(algorithmSpec, "$", 2)
var hasherType string
var config string
if len(vals) == 0 {
// This should not happen as algorithmSpec should not be empty
// due to it being assigned to defaultEmptyHashAlgorithmSpecification above
// but we should be absolutely cautious here
return nil
}
hasherType = vals[0]
if len(vals) > 1 {
config = vals[1]
}
newFn, has := availableHasherFactories[hasherType]
if !has {
// unknown hasher type
return nil
}
ph := newFn(config)
if ph == nil {
// The provided configuration is likely invalid - it will have been logged already
// but we cannot hash safely
return nil
}
hashAlgorithm := &PasswordHashAlgorithm{
PasswordSaltHasher: ph,
Specification: algorithmSpec,
}
lastNonDefaultAlgorithm.Store(hashAlgorithm)
return hashAlgorithm
}

@ -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))
})
}
}
}

@ -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() {
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 {
// 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
}

@ -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() {
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 {
// 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
}

@ -0,0 +1,61 @@
// 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"
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",
}
// SetDefaultPasswordHashAlgorithm will take a provided algorithmName and dealias it to
// a complete algorithm specification.
func SetDefaultPasswordHashAlgorithm(algorithmName string) (string, *PasswordHashAlgorithm) {
if algorithmName == "" {
algorithmName = DefaultHashAlgorithmName
}
alias, has := aliasAlgorithmNames[algorithmName]
for has {
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
}

@ -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_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.Specification, emptyAlgo.Specification)
})
}

@ -11,8 +11,8 @@ import (
"strings" "strings"
"sync" "sync"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/translation"
) )
// complexity contains information about a particular kind of password complexity // 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 // 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 var buffer bytes.Buffer
buffer.WriteString(ctx.Tr("form.password_complexity")) buffer.WriteString(locale.Tr("form.password_complexity"))
buffer.WriteString("<ul>") buffer.WriteString("<ul>")
for _, c := range requiredList { for _, c := range requiredList {
buffer.WriteString("<li>") buffer.WriteString("<li>")
buffer.WriteString(ctx.Tr(c.TrNameOne)) buffer.WriteString(locale.Tr(c.TrNameOne))
buffer.WriteString("</li>") buffer.WriteString("</li>")
} }
buffer.WriteString("</ul>") buffer.WriteString("</ul>")

@ -6,7 +6,7 @@ package password
import ( import (
"context" "context"
"code.gitea.io/gitea/modules/password/pwn" "code.gitea.io/gitea/modules/auth/password/pwn"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
) )

@ -21,6 +21,7 @@ import (
"text/template" "text/template"
"time" "time"
"code.gitea.io/gitea/modules/auth/password/hash"
"code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/generate" "code.gitea.io/gitea/modules/generate"
"code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/json"
@ -968,7 +969,14 @@ func loadFromConf(allowEmpty bool, extraConfig string) {
DisableGitHooks = sec.Key("DISABLE_GIT_HOOKS").MustBool(true) DisableGitHooks = sec.Key("DISABLE_GIT_HOOKS").MustBool(true)
DisableWebhooks = sec.Key("DISABLE_WEBHOOKS").MustBool(false) DisableWebhooks = sec.Key("DISABLE_WEBHOOKS").MustBool(false)
OnlyAllowPushIfGiteaEnvironmentSet = sec.Key("ONLY_ALLOW_PUSH_IF_GITEA_ENVIRONMENT_SET").MustBool(true) 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) CSRFCookieHTTPOnly = sec.Key("CSRF_COOKIE_HTTP_ONLY").MustBool(true)
PasswordCheckPwn = sec.Key("PASSWORD_CHECK_PWN").MustBool(false) PasswordCheckPwn = sec.Key("PASSWORD_CHECK_PWN").MustBool(false)
SuccessfulTokensCacheSize = sec.Key("SUCCESSFUL_TOKENS_CACHE_SIZE").MustInt(20) SuccessfulTokensCacheSize = sec.Key("SUCCESSFUL_TOKENS_CACHE_SIZE").MustInt(20)

@ -15,9 +15,9 @@ import (
"code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user" 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/context"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/password"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"

@ -20,6 +20,7 @@ import (
"code.gitea.io/gitea/models/migrations" "code.gitea.io/gitea/models/migrations"
system_model "code.gitea.io/gitea/models/system" system_model "code.gitea.io/gitea/models/system"
user_model "code.gitea.io/gitea/models/user" 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/base"
"code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/generate" "code.gitea.io/gitea/modules/generate"
@ -79,7 +80,7 @@ func Init(ctx goctx.Context) func(next http.Handler) http.Handler {
"AllLangs": translation.AllLangs(), "AllLangs": translation.AllLangs(),
"PageStartTime": startTime, "PageStartTime": startTime,
"PasswordHashAlgorithms": user_model.AvailableHashAlgorithms, "PasswordHashAlgorithms": hash.RecommendedHashAlgorithms,
}, },
} }
defer ctx.Close() defer ctx.Close()

@ -14,10 +14,10 @@ import (
"code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user" 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/base"
"code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/password"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"

@ -13,11 +13,11 @@ import (
"code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user" 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/base"
"code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/eventsource" "code.gitea.io/gitea/modules/eventsource"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/password"
"code.gitea.io/gitea/modules/session" "code.gitea.io/gitea/modules/session"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"

@ -9,10 +9,10 @@ import (
"code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/auth"
user_model "code.gitea.io/gitea/models/user" 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/base"
"code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/password"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"

@ -11,10 +11,10 @@ import (
"code.gitea.io/gitea/models" "code.gitea.io/gitea/models"
user_model "code.gitea.io/gitea/models/user" 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/base"
"code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/password"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"

Loading…
Cancel
Save