Feature #44750
openSupport Argon2id to make offline password cracking harder
Description
Currently, Redmine stores user passwords as SHA1(salt + SHA1(password)) in users.hashed_password. SHA-1 is a fast general-purpose hash function: according to #24520, even a single GPU could compute about 4.1 billion SHA-1 hashes per second as of December 2016. As each guess takes two SHA-1 computations, about 2 billion guesses per second could be made against a leaked database. The OWASP Password Storage Cheat Sheet recommends Argon2id for password storage. This has been requested in #24520 and #36056, but no patch has been proposed so far.
The attached patch adds Argon2id as a password hash algorithm, selectable with a new password_hash_algorithm setting in config/configuration.yml:
production: password_hash_algorithm: argon2id
The default is sha1, so nothing changes unless the setting is changed. I hope that a future version of Redmine will make argon2id the default.
The patch is based on r25454 of the trunk, and also applies cleanly to r25456.
Behavior¶
- New passwords are hashed with the configured algorithm when they are set, e.g. on account creation, password change and password reset.
- Passwords are verified with the algorithm that hashed the stored value, whatever the setting is. The algorithm is detected from the format of
hashed_password: a 40-character hex string is SHA-1, and a string starting with$argon2id$is Argon2id. - When a user logs in successfully and their password was hashed with an algorithm other than the configured one, it is rehashed with the configured algorithm. So existing users are migrated gradually as they log in, without any batch processing, and switching the setting back to
sha1migrates them back in the same way. - The rehash is saved with
update_columns, so:- it does not change
passwd_changed_on(the password expiration does not move). - it does not destroy the autologin, session and recovery tokens.
- it does not run the validations (a password that does not satisfy the current password policy, or a user with a missing required custom field, does not prevent the login).
- it does not change
- Users authenticated by an LDAP authentication source are not affected.
Implementation¶
- Argon2id hashes are stored in the PHC string format, such as
$argon2id$v=19$m=19456,t=2,p=1$<salt>$<hash>, which includes the salt and the parameters.users.saltis leftNULLfor them. - The migration widens
users.hashed_passwordfrom 40 to 255 characters, as Argon2id hashes are 97 characters long with the default parameters. Rolling it back raisesActiveRecord::IrreversibleMigrationwith the number of affected users if Argon2id hashes exist. - The hashing is moved from
UsertoRedmine::PasswordHash, with one module per algorithm (Redmine::PasswordHash::Sha1andRedmine::PasswordHash::Argon2id) providingmatch?,digest_passwordandverify_password.
Choice of the gem¶
The patch uses the argon2id gem, with its default parameters, which are m=19 MiB, t=2, p=1, as recommended by OWASP.
- The argon2 gem is older and more widely used, but it compiles the C library on installation and does not support Windows. The argon2id gem ships precompiled native gems for Linux (glibc and musl), macOS, Windows and JRuby.
- The OpenSSL extension of Ruby provides scrypt and PBKDF2 (
OpenSSL::KDF), but not Argon2. - bcrypt is already a dependency of Redmine, used by Doorkeeper to hash the secrets of OAuth applications. However, I did not adopt it, because OWASP limits the use of bcrypt to legacy systems where Argon2 and scrypt are not available.
Tests¶
The patch adds the following tests:
- Unit tests for
Redmine::PasswordHashand the algorithm modules. - Functional and integration tests with both algorithms for:
- logging in.
- changing and resetting the password.
- creating and updating users.
- HTTP Basic authentication of the REST API.
- the sudo mode.
Using Redmine on Debian GNU/Linux 13 (trixie) with PostgreSQL 18, I have also confirmed the following on new databases, both switching the setting from sha1 to argon2id and from argon2id to sha1:
db:migratesucceeds on a new database.- The admin and a new user can log in.
- After switching the setting:
- logging in rehashes the password with the new algorithm, on the first login only.
- changing the password hashes it with the new algorithm.
- a user added after the switch has their password hashed with the new algorithm.
- The down migration of
users.hashed_password:- is refused with the number of affected users while Argon2id hashes exist, e.g.
3 user(s) have passwords hashed with Argon2id, reset them with SHA1 first. - succeeds once all the users have been rehashed with SHA-1 by switching the setting to
sha1and logging in.
- is refused with the number of affected users while Argon2id hashes exist, e.g.
With the patch applied on top of r25454, all the tests, including the system tests, pass on GitHub Actions with PostgreSQL, MySQL and SQLite on Ruby 3.3, 3.4 and 4.0:
Files