Feature #44750 ยป support_argon2id.patch
| Gemfile | ||
|---|---|---|
| 23 | 23 |
gem 'commonmarker', '~> 2.10.0' |
| 24 | 24 |
gem 'doorkeeper', '~> 5.9.6' |
| 25 | 25 |
gem "bcrypt", require: false |
| 26 |
gem "argon2id", "~> 0.10.0" |
|
| 26 | 27 |
gem "doorkeeper-i18n", "~> 5.2" |
| 27 | 28 |
gem "requestjs-rails", "~> 0.0.13" |
| 28 | 29 | |
| app/models/user.rb | ||
|---|---|---|
| 243 | 243 |
# user is already in local database |
| 244 | 244 |
return nil unless user.check_password?(password) |
| 245 | 245 |
return nil if !user.active? && active_only |
| 246 | ||
| 247 |
user.rehash_password(password) if user.password_hash_outdated? |
|
| 246 | 248 |
else |
| 247 | 249 |
# user is not yet registered, try to authenticate with available sources |
| 248 | 250 |
attrs = AuthSource.authenticate(login, password) |
| ... | ... | |
| 350 | 352 | |
| 351 | 353 |
# Returns true if +clear_password+ is the correct user's password, otherwise false |
| 352 | 354 |
def check_password?(clear_password) |
| 353 |
if auth_source_id.present? |
|
| 354 |
auth_source.authenticate(self.login, clear_password) |
|
| 355 |
else |
|
| 356 |
User.hash_password("#{salt}#{User.hash_password clear_password}") == hashed_password
|
|
| 357 |
end |
|
| 355 |
return auth_source.authenticate(self.login, clear_password) if auth_source_id.present? |
|
| 356 | ||
| 357 |
algorithm = password_hash_algorithm |
|
| 358 |
return false unless algorithm |
|
| 359 | ||
| 360 |
algorithm.verify_password(clear_password, salt, hashed_password) |
|
| 358 | 361 |
end |
| 359 | 362 | |
| 360 |
# Generates a random salt and computes hashed_password for +clear_password+
|
|
| 361 |
# The hashed password is stored in the following form: SHA1(salt + SHA1(password))
|
|
| 363 |
# Computes salt and hashed_password for +clear_password+ with the configured
|
|
| 364 |
# password hash algorithm, see Redmine::PasswordHash
|
|
| 362 | 365 |
def salt_password(clear_password) |
| 363 |
self.salt = User.generate_salt |
|
| 364 |
self.hashed_password = User.hash_password("#{salt}#{User.hash_password clear_password}")
|
|
| 366 |
self.salt, self.hashed_password = Redmine::PasswordHash.current_algorithm.digest_password(clear_password) |
|
| 365 | 367 |
self.passwd_changed_on = Time.now.change(:usec => 0) |
| 366 | 368 |
end |
| 367 | 369 | |
| 370 |
# Returns true if hashed_password was not computed with the configured |
|
| 371 |
# password hash algorithm |
|
| 372 |
def password_hash_outdated? |
|
| 373 |
return false if auth_source_id.present? || hashed_password.blank? |
|
| 374 | ||
| 375 |
Redmine::PasswordHash.current_algorithm != password_hash_algorithm |
|
| 376 |
end |
|
| 377 | ||
| 378 |
# Returns the algorithm that hashed hashed_password, or nil if the password |
|
| 379 |
# is blank or hashed with an unknown algorithm |
|
| 380 |
def password_hash_algorithm = Redmine::PasswordHash.algorithm_for(hashed_password) |
|
| 381 | ||
| 382 |
# Rehashes +clear_password+ with the configured password hash algorithm |
|
| 383 |
# without changing passwd_changed_on nor invalidating the user's tokens |
|
| 384 |
def rehash_password(clear_password) |
|
| 385 |
salt, hashed_password = Redmine::PasswordHash.current_algorithm.digest_password(clear_password) |
|
| 386 |
update_columns(salt:, hashed_password:) |
|
| 387 |
end |
|
| 388 | ||
| 368 | 389 |
# Does the backend storage allow this user to change their password? |
| 369 | 390 |
def change_password_allowed? |
| 370 | 391 |
auth_source.nil? ? true : auth_source.allow_password_changes? |
| config/configuration.yml.example | ||
|---|---|---|
| 151 | 151 |
# * encrypt data using 'rake db:encrypt RAILS_ENV=production' |
| 152 | 152 |
database_cipher_key: |
| 153 | 153 | |
| 154 |
# Algorithm used to hash the passwords of the users authenticated by |
|
| 155 |
# Redmine itself: sha1 (default) or argon2id. |
|
| 156 |
# Passwords hashed with either algorithm can be verified whatever this |
|
| 157 |
# setting is. When a user logs in, their password is rehashed with the |
|
| 158 |
# algorithm set here if it was hashed with the other one. |
|
| 159 |
# |
|
| 160 |
#password_hash_algorithm: argon2id |
|
| 161 | ||
| 154 | 162 |
# Your secret key for verifying cookie session data integrity. If you |
| 155 | 163 |
# change this key, all old sessions will become invalid! Make sure the |
| 156 | 164 |
# secret is at least 30 characters and all random, no regular words or |
| db/migrate/20261026000000_change_users_hashed_password_limit.rb | ||
|---|---|---|
| 1 |
class ChangeUsersHashedPasswordLimit < ActiveRecord::Migration[8.1] |
|
| 2 |
def up |
|
| 3 |
# Argon2id hashes in the PHC string format are longer than SHA1 hex digests |
|
| 4 |
change_column :users, :hashed_password, :string, limit: 255, default: '', null: false |
|
| 5 |
end |
|
| 6 | ||
| 7 |
def down |
|
| 8 |
# Argon2id hashes do not fit in 40 characters |
|
| 9 |
count = User.where('hashed_password LIKE ?', '$argon2id$%').count
|
|
| 10 |
if count > 0 |
|
| 11 |
raise ActiveRecord::IrreversibleMigration, |
|
| 12 |
"#{count} user(s) have passwords hashed with Argon2id, reset them with SHA1 first"
|
|
| 13 |
end |
|
| 14 | ||
| 15 |
change_column :users, :hashed_password, :string, limit: 40, default: '', null: false |
|
| 16 |
end |
|
| 17 |
end |
|
| lib/redmine/password_hash.rb | ||
|---|---|---|
| 1 |
# frozen_string_literal: true |
|
| 2 | ||
| 3 |
# Redmine - project management software |
|
| 4 |
# Copyright (C) 2006- Jean-Philippe Lang |
|
| 5 |
# |
|
| 6 |
# This program is free software; you can redistribute it and/or |
|
| 7 |
# modify it under the terms of the GNU General Public License |
|
| 8 |
# as published by the Free Software Foundation; either version 2 |
|
| 9 |
# of the License, or (at your option) any later version. |
|
| 10 |
# |
|
| 11 |
# This program is distributed in the hope that it will be useful, |
|
| 12 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
| 13 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
| 14 |
# GNU General Public License for more details. |
|
| 15 |
# |
|
| 16 |
# You should have received a copy of the GNU General Public License |
|
| 17 |
# along with this program; if not, write to the Free Software |
|
| 18 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
| 19 | ||
| 20 |
module Redmine |
|
| 21 |
# Hashes and verifies the passwords of the users authenticated by Redmine |
|
| 22 |
# itself. New passwords are hashed with the algorithm given by the |
|
| 23 |
# password_hash_algorithm setting of config/configuration.yml, and |
|
| 24 |
# passwords hashed with any of ALGORITHMS can be verified. |
|
| 25 |
module PasswordHash |
|
| 26 |
algorithm_names = [ |
|
| 27 |
# Order of strength |
|
| 28 |
:argon2id, |
|
| 29 |
:sha1, |
|
| 30 |
] |
|
| 31 |
ALGORITHMS = algorithm_names.each.with_object({}) do |name, h|
|
|
| 32 |
h[name] = Redmine::PasswordHash.const_get(name.to_s.camelize) |
|
| 33 |
end |
|
| 34 | ||
| 35 |
class << self |
|
| 36 |
# Returns the algorithm given by the password_hash_algorithm setting |
|
| 37 |
def current_algorithm |
|
| 38 |
name = Redmine::Configuration['password_hash_algorithm'].presence&.to_sym || :sha1 |
|
| 39 |
ALGORITHMS[name] || raise(ArgumentError, "Unsupported password hash algorithm: name=#{name}")
|
|
| 40 |
end |
|
| 41 | ||
| 42 |
# Returns the algorithm that hashed +hashed_password+, or nil |
|
| 43 |
def algorithm_for(hashed_password) |
|
| 44 |
ALGORITHMS.values.find {|algorithm| algorithm.match?(hashed_password)}
|
|
| 45 |
end |
|
| 46 |
end |
|
| 47 |
end |
|
| 48 |
end |
|
| lib/redmine/password_hash/argon2id.rb | ||
|---|---|---|
| 1 |
# frozen_string_literal: true |
|
| 2 | ||
| 3 |
# Redmine - project management software |
|
| 4 |
# Copyright (C) 2006- Jean-Philippe Lang |
|
| 5 |
# |
|
| 6 |
# This program is free software; you can redistribute it and/or |
|
| 7 |
# modify it under the terms of the GNU General Public License |
|
| 8 |
# as published by the Free Software Foundation; either version 2 |
|
| 9 |
# of the License, or (at your option) any later version. |
|
| 10 |
# |
|
| 11 |
# This program is distributed in the hope that it will be useful, |
|
| 12 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
| 13 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
| 14 |
# GNU General Public License for more details. |
|
| 15 |
# |
|
| 16 |
# You should have received a copy of the GNU General Public License |
|
| 17 |
# along with this program; if not, write to the Free Software |
|
| 18 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
| 19 | ||
| 20 |
module Redmine |
|
| 21 |
module PasswordHash |
|
| 22 |
# Stores the PHC string format (e.g. "$argon2id$v=19$m=...") that includes |
|
| 23 |
# the salt and the parameters, so the salt is left blank |
|
| 24 |
module Argon2id |
|
| 25 |
class << self |
|
| 26 |
def match?(hashed_password) = hashed_password.to_s.start_with?('$argon2id$')
|
|
| 27 | ||
| 28 |
# Returns [salt, hashed_password] for +clear_password+ |
|
| 29 |
def digest_password(clear_password) |
|
| 30 |
[nil, ::Argon2id::Password.create(clear_password.to_s).to_s] |
|
| 31 |
end |
|
| 32 | ||
| 33 |
# Returns true if +clear_password+ matches +hashed_password+ |
|
| 34 |
def verify_password(clear_password, salt, hashed_password) |
|
| 35 |
::Argon2id::Password.new(hashed_password).is_password?(clear_password.to_s) |
|
| 36 |
rescue ArgumentError, ::Argon2id::Error |
|
| 37 |
false |
|
| 38 |
end |
|
| 39 |
end |
|
| 40 |
end |
|
| 41 |
end |
|
| 42 |
end |
|
| lib/redmine/password_hash/sha1.rb | ||
|---|---|---|
| 1 |
# frozen_string_literal: true |
|
| 2 | ||
| 3 |
# Redmine - project management software |
|
| 4 |
# Copyright (C) 2006- Jean-Philippe Lang |
|
| 5 |
# |
|
| 6 |
# This program is free software; you can redistribute it and/or |
|
| 7 |
# modify it under the terms of the GNU General Public License |
|
| 8 |
# as published by the Free Software Foundation; either version 2 |
|
| 9 |
# of the License, or (at your option) any later version. |
|
| 10 |
# |
|
| 11 |
# This program is distributed in the hope that it will be useful, |
|
| 12 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
| 13 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
| 14 |
# GNU General Public License for more details. |
|
| 15 |
# |
|
| 16 |
# You should have received a copy of the GNU General Public License |
|
| 17 |
# along with this program; if not, write to the Free Software |
|
| 18 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
| 19 | ||
| 20 |
module Redmine |
|
| 21 |
module PasswordHash |
|
| 22 |
# Stores SHA1(salt + SHA1(password)) as a hex string with a random salt |
|
| 23 |
module Sha1 |
|
| 24 |
class << self |
|
| 25 |
def match?(hashed_password) = /\A\h{40}\z/.match?(hashed_password.to_s)
|
|
| 26 | ||
| 27 |
# Returns [salt, hashed_password] for +clear_password+ |
|
| 28 |
def digest_password(clear_password) |
|
| 29 |
salt = Redmine::Utils.random_hex(16) |
|
| 30 |
[salt, hash("#{salt}#{hash(clear_password)}")]
|
|
| 31 |
end |
|
| 32 | ||
| 33 |
# Returns true if +clear_password+ matches +hashed_password+ |
|
| 34 |
def verify_password(clear_password, salt, hashed_password) |
|
| 35 |
hash("#{salt}#{hash(clear_password)}") == hashed_password
|
|
| 36 |
end |
|
| 37 | ||
| 38 |
private |
|
| 39 | ||
| 40 |
def hash(value) |
|
| 41 |
Digest::SHA1.hexdigest(value || '') |
|
| 42 |
end |
|
| 43 |
end |
|
| 44 |
end |
|
| 45 |
end |
|
| 46 |
end |
|
| test/functional/account_controller_test.rb | ||
|---|---|---|
| 173 | 173 |
assert_select 'input[name=password][value]', 0 |
| 174 | 174 |
end |
| 175 | 175 | |
| 176 |
def test_login_should_rehash_sha1_password_with_argon2id |
|
| 177 |
Redmine::Configuration.with 'password_hash_algorithm' => 'argon2id' do |
|
| 178 |
post :login, params: {username: 'jsmith', password: 'jsmith'}
|
|
| 179 |
assert_redirected_to '/my/page' |
|
| 180 |
end |
|
| 181 |
assert Redmine::PasswordHash::Argon2id.match?(User.find(2).hashed_password) |
|
| 182 |
end |
|
| 183 | ||
| 184 |
def test_login_should_rehash_argon2id_password_with_sha1 |
|
| 185 |
hash_password_with :argon2id, User.find(2), 'jsmith' |
|
| 186 |
Redmine::Configuration.with 'password_hash_algorithm' => 'sha1' do |
|
| 187 |
post :login, params: {username: 'jsmith', password: 'jsmith'}
|
|
| 188 |
assert_redirected_to '/my/page' |
|
| 189 |
end |
|
| 190 |
assert Redmine::PasswordHash::Sha1.match?(User.find(2).hashed_password) |
|
| 191 |
end |
|
| 192 | ||
| 193 |
def test_login_with_argon2id_password |
|
| 194 |
hash_password_with :argon2id, User.find(2), 'jsmith' |
|
| 195 |
hashed_password = User.find(2).hashed_password |
|
| 196 |
Redmine::Configuration.with 'password_hash_algorithm' => 'argon2id' do |
|
| 197 |
post :login, params: {username: 'jsmith', password: 'jsmith'}
|
|
| 198 |
assert_redirected_to '/my/page' |
|
| 199 |
end |
|
| 200 |
assert_equal hashed_password, User.find(2).hashed_password |
|
| 201 |
end |
|
| 202 | ||
| 203 |
def test_login_with_wrong_password_should_not_rehash_password |
|
| 204 |
hashed_password = User.find(2).hashed_password |
|
| 205 |
Redmine::Configuration.with 'password_hash_algorithm' => 'argon2id' do |
|
| 206 |
post :login, params: {username: 'jsmith', password: 'bad'}
|
|
| 207 |
assert_response :success |
|
| 208 |
assert_select 'div.flash.error', text: /Invalid user or password/ |
|
| 209 |
end |
|
| 210 |
assert_equal hashed_password, User.find(2).hashed_password |
|
| 211 |
end |
|
| 212 | ||
| 176 | 213 |
def test_login_with_locked_account_should_fail |
| 177 | 214 |
User.find(2).update_attribute :status, User::STATUS_LOCKED |
| 178 | 215 |
post( |
| ... | ... | |
| 587 | 624 |
end |
| 588 | 625 |
end |
| 589 | 626 | |
| 627 |
def test_post_lost_password_with_token_should_hash_password_with_configured_algorithm |
|
| 628 |
user = User.find(2) |
|
| 629 |
token = Token.create!(action: 'recovery', user:) |
|
| 630 |
Redmine::Configuration.with 'password_hash_algorithm' => 'argon2id' do |
|
| 631 |
post( |
|
| 632 |
:lost_password, |
|
| 633 |
params: {
|
|
| 634 |
token: token.value, |
|
| 635 |
new_password: 'newpass123', |
|
| 636 |
new_password_confirmation: 'newpass123' |
|
| 637 |
} |
|
| 638 |
) |
|
| 639 |
assert_redirected_to '/login' |
|
| 640 |
end |
|
| 641 |
user.reload |
|
| 642 |
assert Redmine::PasswordHash::Argon2id.match?(user.hashed_password) |
|
| 643 |
assert user.check_password?('newpass123')
|
|
| 644 |
end |
|
| 645 | ||
| 590 | 646 |
def test_post_lost_password_with_token_for_non_active_user_should_fail |
| 591 | 647 |
user = User.find(2) |
| 592 | 648 |
token = Token.create!(:action => 'recovery', :user => user) |
| test/functional/my_controller_test.rb | ||
|---|---|---|
| 693 | 693 |
assert User.try_to_login('jsmith', 'secret123')
|
| 694 | 694 |
end |
| 695 | 695 | |
| 696 |
def test_update_password_should_hash_password_with_configured_algorithm |
|
| 697 |
Redmine::Configuration.with 'password_hash_algorithm' => 'argon2id' do |
|
| 698 |
post( |
|
| 699 |
:password, |
|
| 700 |
params: {
|
|
| 701 |
password: 'jsmith', |
|
| 702 |
new_password: 'secret123', |
|
| 703 |
new_password_confirmation: 'secret123' |
|
| 704 |
} |
|
| 705 |
) |
|
| 706 |
assert_redirected_to '/my/account' |
|
| 707 |
end |
|
| 708 |
user = User.find(2) |
|
| 709 |
assert Redmine::PasswordHash::Argon2id.match?(user.hashed_password) |
|
| 710 |
assert user.check_password?('secret123')
|
|
| 711 |
end |
|
| 712 | ||
| 713 |
def test_update_password_with_argon2id_password |
|
| 714 |
hash_password_with :argon2id, User.find(2), 'jsmith' |
|
| 715 |
post( |
|
| 716 |
:password, |
|
| 717 |
params: {
|
|
| 718 |
password: 'jsmith', |
|
| 719 |
new_password: 'secret123', |
|
| 720 |
new_password_confirmation: 'secret123' |
|
| 721 |
} |
|
| 722 |
) |
|
| 723 |
assert_redirected_to '/my/account' |
|
| 724 |
assert User.try_to_login('jsmith', 'secret123')
|
|
| 725 |
end |
|
| 726 | ||
| 727 |
def test_update_password_with_wrong_argon2id_password |
|
| 728 |
hash_password_with :argon2id, User.find(2), 'jsmith' |
|
| 729 |
post( |
|
| 730 |
:password, |
|
| 731 |
params: {
|
|
| 732 |
password: 'wrongpassword', |
|
| 733 |
new_password: 'secret123', |
|
| 734 |
new_password_confirmation: 'secret123' |
|
| 735 |
} |
|
| 736 |
) |
|
| 737 |
assert_response :success |
|
| 738 |
assert_equal 'Wrong password', flash[:error] |
|
| 739 |
assert User.try_to_login('jsmith', 'jsmith')
|
|
| 740 |
end |
|
| 741 | ||
| 696 | 742 |
def test_update_password_with_non_matching_confirmation |
| 697 | 743 |
post( |
| 698 | 744 |
:password, |
| test/functional/users_controller_test.rb | ||
|---|---|---|
| 502 | 502 |
assert_mail_body_match 'secret', mail |
| 503 | 503 |
end |
| 504 | 504 | |
| 505 |
def test_create_should_hash_password_with_configured_algorithm |
|
| 506 |
Redmine::Configuration.with 'password_hash_algorithm' => 'argon2id' do |
|
| 507 |
assert_difference 'User.count' do |
|
| 508 |
post :create, params: {
|
|
| 509 |
user: {
|
|
| 510 |
firstname: 'John', |
|
| 511 |
lastname: 'Doe', |
|
| 512 |
login: 'jdoe', |
|
| 513 |
password: 'secret123', |
|
| 514 |
password_confirmation: 'secret123', |
|
| 515 |
mail: 'jdoe@gmail.com', |
|
| 516 |
mail_notification: 'none' |
|
| 517 |
} |
|
| 518 |
} |
|
| 519 |
end |
|
| 520 |
end |
|
| 521 |
user = User.find_by_login('jdoe')
|
|
| 522 |
assert Redmine::PasswordHash::Argon2id.match?(user.hashed_password) |
|
| 523 |
assert_nil user.salt |
|
| 524 |
assert user.check_password?('secret123')
|
|
| 525 |
end |
|
| 526 | ||
| 505 | 527 |
def test_create_with_preferences |
| 506 | 528 |
assert_difference 'User.count' do |
| 507 | 529 |
post :create, :params => {
|
| ... | ... | |
| 804 | 826 |
assert_mail_body_match 'Your password has been changed.', mail |
| 805 | 827 |
end |
| 806 | 828 | |
| 829 |
def test_update_with_password_change_should_hash_password_with_configured_algorithm |
|
| 830 |
Redmine::Configuration.with 'password_hash_algorithm' => 'argon2id' do |
|
| 831 |
put :update, params: {
|
|
| 832 |
id: 2, |
|
| 833 |
user: {password: 'newpass123', password_confirmation: 'newpass123'}
|
|
| 834 |
} |
|
| 835 |
assert_redirected_to '/users/2/edit' |
|
| 836 |
end |
|
| 837 |
user = User.find(2) |
|
| 838 |
assert Redmine::PasswordHash::Argon2id.match?(user.hashed_password) |
|
| 839 |
assert user.check_password?('newpass123')
|
|
| 840 |
end |
|
| 841 | ||
| 807 | 842 |
def test_update_with_generate_password_should_email_the_password |
| 808 | 843 |
ActionMailer::Base.deliveries.clear |
| 809 | 844 | |
| test/integration/account_test.rb | ||
|---|---|---|
| 84 | 84 |
Redmine::Configuration.stubs(:[]).with('autologin_cookie_path').returns('/')
|
| 85 | 85 |
Redmine::Configuration.stubs(:[]).with('autologin_cookie_secure').returns(false)
|
| 86 | 86 |
Redmine::Configuration.stubs(:[]).with('sudo_mode_timeout').returns(15)
|
| 87 |
Redmine::Configuration.stubs(:[]).with('password_hash_algorithm').returns(nil)
|
|
| 87 | 88 | |
| 88 | 89 |
with_settings :autologin => '7' do |
| 89 | 90 |
assert_difference 'Token.count', 2 do |
| test/integration/api_test/authentication_test.rb | ||
|---|---|---|
| 47 | 47 |
assert response.headers.has_key?('WWW-Authenticate')
|
| 48 | 48 |
end |
| 49 | 49 | |
| 50 |
def test_api_should_accept_http_basic_auth_using_username_and_argon2id_password |
|
| 51 |
user = User.generate! |
|
| 52 |
hash_password_with :argon2id, user, 'my_password' |
|
| 53 |
get '/users/current.xml', headers: credentials(user.login, 'my_password') |
|
| 54 |
assert_response :ok |
|
| 55 |
end |
|
| 56 | ||
| 57 |
def test_api_should_deny_http_basic_auth_using_username_and_wrong_argon2id_password |
|
| 58 |
user = User.generate! |
|
| 59 |
hash_password_with :argon2id, user, 'my_password' |
|
| 60 |
get '/users/current.xml', headers: credentials(user.login, 'wrong_password') |
|
| 61 |
assert_response :unauthorized |
|
| 62 |
end |
|
| 63 | ||
| 64 |
def test_api_http_basic_auth_should_rehash_password_with_configured_algorithm |
|
| 65 |
user = User.generate! do |user| |
|
| 66 |
user.password = 'my_password' |
|
| 67 |
end |
|
| 68 |
Redmine::Configuration.with 'password_hash_algorithm' => 'argon2id' do |
|
| 69 |
get '/users/current.xml', headers: credentials(user.login, 'my_password') |
|
| 70 |
assert_response :ok |
|
| 71 |
end |
|
| 72 |
assert Redmine::PasswordHash::Argon2id.match?(user.reload.hashed_password) |
|
| 73 |
end |
|
| 74 | ||
| 50 | 75 |
def test_api_should_deny_http_basic_auth_if_twofa_is_active |
| 51 | 76 |
user = User.generate! do |user| |
| 52 | 77 |
user.password = 'my_password' |
| test/integration/sudo_mode_test.rb | ||
|---|---|---|
| 72 | 72 |
assert_kind_of User, user |
| 73 | 73 |
end |
| 74 | 74 | |
| 75 |
def test_add_user_with_argon2id_password |
|
| 76 |
hash_password_with :argon2id, User.find(1), 'admin' |
|
| 77 |
Redmine::Configuration.with 'password_hash_algorithm' => 'argon2id' do |
|
| 78 |
log_user("admin", "admin")
|
|
| 79 |
expire_sudo_mode! |
|
| 80 |
assert Redmine::PasswordHash::Argon2id.match?(User.find(1).hashed_password) |
|
| 81 | ||
| 82 |
user_params = {
|
|
| 83 |
login: "psmith", firstname: "Paul", |
|
| 84 |
lastname: "Smith", mail: "psmith@somenet.foo", |
|
| 85 |
language: "en", password: "psmith09", |
|
| 86 |
password_confirmation: "psmith09" |
|
| 87 |
} |
|
| 88 |
post "/users", params: {user: user_params, sudo_password: 'wrong'}
|
|
| 89 |
assert_response :success |
|
| 90 |
assert_nil User.find_by_login("psmith")
|
|
| 91 | ||
| 92 |
post "/users", params: {user: user_params, sudo_password: 'admin'}
|
|
| 93 |
assert_response :found |
|
| 94 |
assert_kind_of User, User.find_by_login("psmith")
|
|
| 95 |
end |
|
| 96 |
end |
|
| 97 | ||
| 75 | 98 |
def test_create_member_xhr |
| 76 | 99 |
log_user 'admin', 'admin' |
| 77 | 100 |
expire_sudo_mode! |
| test/test_helper.rb | ||
|---|---|---|
| 146 | 146 |
saved_settings.each {|k, v| Setting[k] = v} if saved_settings
|
| 147 | 147 |
end |
| 148 | 148 | |
| 149 |
# Stores password of user hashed with the password hash algorithm name, |
|
| 150 |
# whatever the password_hash_algorithm setting is |
|
| 151 |
def hash_password_with(name, user, password) |
|
| 152 |
salt, hashed_password = Redmine::PasswordHash::ALGORITHMS.fetch(name).digest_password(password) |
|
| 153 |
user.update_columns(salt:, hashed_password:) |
|
| 154 |
end |
|
| 155 | ||
| 149 | 156 |
# Yields the block with user as the current user |
| 150 | 157 |
def with_current_user(user, &) |
| 151 | 158 |
saved_user = User.current |
| test/unit/lib/redmine/password_hash/argon2id_test.rb | ||
|---|---|---|
| 1 |
# frozen_string_literal: true |
|
| 2 | ||
| 3 |
# Redmine - project management software |
|
| 4 |
# Copyright (C) 2006- Jean-Philippe Lang |
|
| 5 |
# |
|
| 6 |
# This program is free software; you can redistribute it and/or |
|
| 7 |
# modify it under the terms of the GNU General Public License |
|
| 8 |
# as published by the Free Software Foundation; either version 2 |
|
| 9 |
# of the License, or (at your option) any later version. |
|
| 10 |
# |
|
| 11 |
# This program is distributed in the hope that it will be useful, |
|
| 12 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
| 13 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
| 14 |
# GNU General Public License for more details. |
|
| 15 |
# |
|
| 16 |
# You should have received a copy of the GNU General Public License |
|
| 17 |
# along with this program; if not, write to the Free Software |
|
| 18 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
| 19 | ||
| 20 |
require_relative '../../../../test_helper' |
|
| 21 | ||
| 22 |
class Redmine::PasswordHash::Argon2idTest < ActiveSupport::TestCase |
|
| 23 |
def test_s_digest_password |
|
| 24 |
salt, hashed_password = Redmine::PasswordHash::Argon2id.digest_password('hello123')
|
|
| 25 | ||
| 26 |
assert_nil salt |
|
| 27 |
assert hashed_password.start_with?('$argon2id$')
|
|
| 28 |
end |
|
| 29 | ||
| 30 |
def test_s_match_p |
|
| 31 |
_, hashed_password = Redmine::PasswordHash::Argon2id.digest_password('hello123')
|
|
| 32 |
_, sha1 = Redmine::PasswordHash::Sha1.digest_password('hello123')
|
|
| 33 | ||
| 34 |
assert Redmine::PasswordHash::Argon2id.match?(hashed_password) |
|
| 35 |
assert_not Redmine::PasswordHash::Argon2id.match?(sha1) |
|
| 36 |
assert_not Redmine::PasswordHash::Argon2id.match?('')
|
|
| 37 |
assert_not Redmine::PasswordHash::Argon2id.match?(nil) |
|
| 38 |
end |
|
| 39 | ||
| 40 |
def test_s_verify_password |
|
| 41 |
_, hashed_password = Redmine::PasswordHash::Argon2id.digest_password('hello123')
|
|
| 42 | ||
| 43 |
assert Redmine::PasswordHash::Argon2id.verify_password('hello123', nil, hashed_password)
|
|
| 44 |
assert_not Redmine::PasswordHash::Argon2id.verify_password('hello124', nil, hashed_password)
|
|
| 45 |
end |
|
| 46 | ||
| 47 |
def test_s_verify_password_should_return_false_with_broken_hash |
|
| 48 |
assert_not Redmine::PasswordHash::Argon2id.verify_password('hello123', nil, '$argon2id$broken')
|
|
| 49 |
end |
|
| 50 |
end |
|
| test/unit/lib/redmine/password_hash/sha1_test.rb | ||
|---|---|---|
| 1 |
# frozen_string_literal: true |
|
| 2 | ||
| 3 |
# Redmine - project management software |
|
| 4 |
# Copyright (C) 2006- Jean-Philippe Lang |
|
| 5 |
# |
|
| 6 |
# This program is free software; you can redistribute it and/or |
|
| 7 |
# modify it under the terms of the GNU General Public License |
|
| 8 |
# as published by the Free Software Foundation; either version 2 |
|
| 9 |
# of the License, or (at your option) any later version. |
|
| 10 |
# |
|
| 11 |
# This program is distributed in the hope that it will be useful, |
|
| 12 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
| 13 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
| 14 |
# GNU General Public License for more details. |
|
| 15 |
# |
|
| 16 |
# You should have received a copy of the GNU General Public License |
|
| 17 |
# along with this program; if not, write to the Free Software |
|
| 18 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
| 19 | ||
| 20 |
require_relative '../../../../test_helper' |
|
| 21 | ||
| 22 |
class Redmine::PasswordHash::Sha1Test < ActiveSupport::TestCase |
|
| 23 |
def test_s_digest_password |
|
| 24 |
salt, hashed_password = Redmine::PasswordHash::Sha1.digest_password('hello123')
|
|
| 25 | ||
| 26 |
assert_match /\A\h{32}\z/, salt
|
|
| 27 |
assert_match /\A\h{40}\z/, hashed_password
|
|
| 28 |
end |
|
| 29 | ||
| 30 |
def test_s_match_p |
|
| 31 |
_, hashed_password = Redmine::PasswordHash::Sha1.digest_password('hello123')
|
|
| 32 |
_, argon2id = Redmine::PasswordHash::Argon2id.digest_password('hello123')
|
|
| 33 | ||
| 34 |
assert Redmine::PasswordHash::Sha1.match?(hashed_password) |
|
| 35 |
assert_not Redmine::PasswordHash::Sha1.match?(argon2id) |
|
| 36 |
assert_not Redmine::PasswordHash::Sha1.match?('')
|
|
| 37 |
assert_not Redmine::PasswordHash::Sha1.match?(nil) |
|
| 38 |
end |
|
| 39 | ||
| 40 |
def test_s_verify_password |
|
| 41 |
salt, hashed_password = Redmine::PasswordHash::Sha1.digest_password('hello123')
|
|
| 42 | ||
| 43 |
assert Redmine::PasswordHash::Sha1.verify_password('hello123', salt, hashed_password)
|
|
| 44 |
assert_not Redmine::PasswordHash::Sha1.verify_password('hello124', salt, hashed_password)
|
|
| 45 |
end |
|
| 46 |
end |
|
| test/unit/lib/redmine/password_hash_test.rb | ||
|---|---|---|
| 1 |
# frozen_string_literal: true |
|
| 2 | ||
| 3 |
# Redmine - project management software |
|
| 4 |
# Copyright (C) 2006- Jean-Philippe Lang |
|
| 5 |
# |
|
| 6 |
# This program is free software; you can redistribute it and/or |
|
| 7 |
# modify it under the terms of the GNU General Public License |
|
| 8 |
# as published by the Free Software Foundation; either version 2 |
|
| 9 |
# of the License, or (at your option) any later version. |
|
| 10 |
# |
|
| 11 |
# This program is distributed in the hope that it will be useful, |
|
| 12 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
| 13 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
| 14 |
# GNU General Public License for more details. |
|
| 15 |
# |
|
| 16 |
# You should have received a copy of the GNU General Public License |
|
| 17 |
# along with this program; if not, write to the Free Software |
|
| 18 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
| 19 | ||
| 20 |
require_relative '../../../test_helper' |
|
| 21 | ||
| 22 |
class Redmine::PasswordHashTest < ActiveSupport::TestCase |
|
| 23 |
def test_s_current_algorithm_should_return_configured_algorithm |
|
| 24 |
Redmine::Configuration.with 'password_hash_algorithm' => 'sha1' do |
|
| 25 |
assert_equal Redmine::PasswordHash::Sha1, Redmine::PasswordHash.current_algorithm |
|
| 26 |
end |
|
| 27 |
Redmine::Configuration.with 'password_hash_algorithm' => 'argon2id' do |
|
| 28 |
assert_equal Redmine::PasswordHash::Argon2id, Redmine::PasswordHash.current_algorithm |
|
| 29 |
end |
|
| 30 |
end |
|
| 31 | ||
| 32 |
def test_s_current_algorithm_should_return_sha1_by_default |
|
| 33 |
Redmine::Configuration.with 'password_hash_algorithm' => nil do |
|
| 34 |
assert_equal Redmine::PasswordHash::Sha1, Redmine::PasswordHash.current_algorithm |
|
| 35 |
end |
|
| 36 |
end |
|
| 37 | ||
| 38 |
def test_s_current_algorithm_should_raise_error_with_unsupported_algorithm |
|
| 39 |
Redmine::Configuration.with 'password_hash_algorithm' => 'md5' do |
|
| 40 |
error = assert_raise ArgumentError do |
|
| 41 |
Redmine::PasswordHash.current_algorithm |
|
| 42 |
end |
|
| 43 |
assert_equal 'Unsupported password hash algorithm: name=md5', error.message |
|
| 44 |
end |
|
| 45 |
end |
|
| 46 | ||
| 47 |
def test_s_algorithm_for |
|
| 48 |
_, sha1 = Redmine::PasswordHash::Sha1.digest_password('hello123')
|
|
| 49 |
_, argon2id = Redmine::PasswordHash::Argon2id.digest_password('hello123')
|
|
| 50 | ||
| 51 |
%w[sha1 argon2id].each do |name| |
|
| 52 |
Redmine::Configuration.with 'password_hash_algorithm' => name do |
|
| 53 |
assert_equal Redmine::PasswordHash::Sha1, Redmine::PasswordHash.algorithm_for(sha1) |
|
| 54 |
assert_equal Redmine::PasswordHash::Argon2id, Redmine::PasswordHash.algorithm_for(argon2id) |
|
| 55 |
end |
|
| 56 |
end |
|
| 57 |
end |
|
| 58 | ||
| 59 |
def test_s_algorithm_for_should_return_nil_with_unknown_hash |
|
| 60 |
assert_nil Redmine::PasswordHash.algorithm_for('')
|
|
| 61 |
assert_nil Redmine::PasswordHash.algorithm_for(nil) |
|
| 62 |
assert_nil Redmine::PasswordHash.algorithm_for('$2a$12$unknown')
|
|
| 63 |
end |
|
| 64 |
end |
|
| test/unit/user_test.rb | ||
|---|---|---|
| 613 | 613 |
assert_equal "admin", user.login |
| 614 | 614 |
end |
| 615 | 615 | |
| 616 |
def test_password_with_argon2id |
|
| 617 |
Redmine::Configuration.with 'password_hash_algorithm' => 'argon2id' do |
|
| 618 |
user = User.find(1) |
|
| 619 |
user.password = "hello123" |
|
| 620 |
user.save! |
|
| 621 | ||
| 622 |
user.reload |
|
| 623 |
assert user.hashed_password.start_with?('$argon2id$')
|
|
| 624 |
assert_nil user.salt |
|
| 625 |
assert user.check_password?("hello123")
|
|
| 626 |
assert_not user.check_password?("hello124")
|
|
| 627 |
assert_not user.password_hash_outdated? |
|
| 628 |
assert_equal user, User.try_to_login("admin", "hello123")
|
|
| 629 |
assert_nil User.try_to_login("admin", "hello124")
|
|
| 630 |
end |
|
| 631 |
end |
|
| 632 | ||
| 633 |
def test_argon2id_password_should_be_verified_when_algorithm_is_sha1 |
|
| 634 |
user = User.find(1) |
|
| 635 |
Redmine::Configuration.with 'password_hash_algorithm' => 'argon2id' do |
|
| 636 |
user.password = "hello123" |
|
| 637 |
user.save! |
|
| 638 |
end |
|
| 639 | ||
| 640 |
Redmine::Configuration.with 'password_hash_algorithm' => 'sha1' do |
|
| 641 |
user.reload |
|
| 642 |
assert user.check_password?("hello123")
|
|
| 643 |
assert user.password_hash_outdated? |
|
| 644 |
end |
|
| 645 |
end |
|
| 646 | ||
| 647 |
def test_try_to_login_should_rehash_sha1_password_with_argon2id |
|
| 648 |
user = User.find(1) |
|
| 649 |
token = Token.create!(user:, action: 'autologin') |
|
| 650 |
passwd_changed_on = 1.day.ago.change(usec: 0) |
|
| 651 |
user.update_column(:passwd_changed_on, passwd_changed_on) |
|
| 652 | ||
| 653 |
Redmine::Configuration.with 'password_hash_algorithm' => 'argon2id' do |
|
| 654 |
assert_equal user, User.try_to_login("admin", "admin")
|
|
| 655 | ||
| 656 |
user.reload |
|
| 657 |
assert user.hashed_password.start_with?('$argon2id$')
|
|
| 658 |
assert_nil user.salt |
|
| 659 |
assert_equal passwd_changed_on, user.passwd_changed_on |
|
| 660 |
assert Token.exists?(token.id) |
|
| 661 |
assert_equal user, User.try_to_login("admin", "admin")
|
|
| 662 |
end |
|
| 663 |
end |
|
| 664 | ||
| 665 |
def test_try_to_login_should_rehash_argon2id_password_with_sha1 |
|
| 666 |
user = User.find(1) |
|
| 667 |
Redmine::Configuration.with 'password_hash_algorithm' => 'argon2id' do |
|
| 668 |
user.password = "hello123" |
|
| 669 |
user.save! |
|
| 670 |
end |
|
| 671 | ||
| 672 |
Redmine::Configuration.with 'password_hash_algorithm' => 'sha1' do |
|
| 673 |
assert_equal user, User.try_to_login("admin", "hello123")
|
|
| 674 | ||
| 675 |
user.reload |
|
| 676 |
assert_match /\A\h{40}\z/, user.hashed_password
|
|
| 677 |
assert user.salt.present? |
|
| 678 |
assert_equal user, User.try_to_login("admin", "hello123")
|
|
| 679 |
end |
|
| 680 |
end |
|
| 681 | ||
| 682 |
def test_check_password_should_return_false_without_hashed_password |
|
| 683 |
user = User.find(1) |
|
| 684 |
user.hashed_password = '' |
|
| 685 | ||
| 686 |
assert_not user.check_password?('admin')
|
|
| 687 |
assert_not user.check_password?('')
|
|
| 688 |
end |
|
| 689 | ||
| 690 |
def test_check_password_should_return_false_with_unknown_hashed_password |
|
| 691 |
user = User.find(1) |
|
| 692 |
user.hashed_password = '$2a$12$unknown' |
|
| 693 | ||
| 694 |
assert_not user.check_password?('admin')
|
|
| 695 |
end |
|
| 696 | ||
| 697 |
def test_try_to_login_should_return_nil_without_hashed_password |
|
| 698 |
User.find(1).update_columns(hashed_password: '') |
|
| 699 | ||
| 700 |
assert_nil User.try_to_login('admin', 'admin')
|
|
| 701 |
end |
|
| 702 | ||
| 703 |
def test_password_hash_outdated_p_should_return_false_without_hashed_password |
|
| 704 |
user = User.find(1) |
|
| 705 |
user.hashed_password = '' |
|
| 706 | ||
| 707 |
Redmine::Configuration.with 'password_hash_algorithm' => 'argon2id' do |
|
| 708 |
assert_not user.password_hash_outdated? |
|
| 709 |
end |
|
| 710 |
end |
|
| 711 | ||
| 712 |
def test_password_hash_outdated_p_should_return_false_with_auth_source |
|
| 713 |
user = User.find(1) |
|
| 714 |
user.auth_source_id = 1 |
|
| 715 | ||
| 716 |
Redmine::Configuration.with 'password_hash_algorithm' => 'argon2id' do |
|
| 717 |
assert_not user.password_hash_outdated? |
|
| 718 |
end |
|
| 719 |
end |
|
| 720 | ||
| 721 |
def test_try_to_login_should_not_rehash_password_with_wrong_password |
|
| 722 |
user = User.find(1) |
|
| 723 |
Redmine::Configuration.with 'password_hash_algorithm' => 'argon2id' do |
|
| 724 |
assert_nil User.try_to_login("admin", "wrong")
|
|
| 725 |
assert_equal user.hashed_password, user.reload.hashed_password |
|
| 726 |
end |
|
| 727 |
end |
|
| 728 | ||
| 616 | 729 |
def test_validate_password_length |
| 617 | 730 |
with_settings :password_min_length => '100' do |
| 618 | 731 |
user = User.new(:firstname => "new100", |