Project

General

Profile

Feature #44750 ยป support_argon2id.patch

Nishida Yuya, 2026-11-14 11:06

View differences:

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",
    (1-1/1)