diff options
author | zeripath <art27@cantab.net> | 2023-02-19 07:35:52 +0000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-02-19 15:35:52 +0800 |
commit | c702e7995da6f1a6f5ab5137aea965a5f10e08e5 (patch) | |
tree | 7d3bc3ad65321aef8b6da624565d7b9218320e8c | |
parent | b2e58edd74f8d9bb530335e9b1ee2e356d4dacb4 (diff) | |
download | gitea-c702e7995da6f1a6f5ab5137aea965a5f10e08e5.tar.gz gitea-c702e7995da6f1a6f5ab5137aea965a5f10e08e5.zip |
Provide the ability to set password hash algorithm parameters (#22942) (#22943)
Backport #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: Lunny Xiao <xiaolunwen@gmail.com>
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" |