@@ -0,0 +1,9 | |||
|
1 | class AddUsersSalt < ActiveRecord::Migration | |
|
2 | def self.up | |
|
3 | add_column :users, :salt, :string, :limit => 64 | |
|
4 | end | |
|
5 | ||
|
6 | def self.down | |
|
7 | remove_column :users, :salt | |
|
8 | end | |
|
9 | end |
@@ -0,0 +1,13 | |||
|
1 | class SaltUserPasswords < ActiveRecord::Migration | |
|
2 | ||
|
3 | def self.up | |
|
4 | say_with_time "Salting user passwords, this may take some time..." do | |
|
5 | User.salt_unsalted_passwords! | |
|
6 | end | |
|
7 | end | |
|
8 | ||
|
9 | def self.down | |
|
10 | # Unsalted passwords can not be restored | |
|
11 | raise ActiveRecord::IrreversibleMigration, "Can't decypher salted passwords. This migration can not be rollback'ed." | |
|
12 | end | |
|
13 | end |
@@ -83,7 +83,9 class User < Principal | |||
|
83 | 83 | |
|
84 | 84 | def before_save |
|
85 | 85 | # update hashed_password if password was set |
|
86 |
|
|
|
86 | if self.password && self.auth_source_id.blank? | |
|
87 | salt_password(password) | |
|
88 | end | |
|
87 | 89 | end |
|
88 | 90 | |
|
89 | 91 | def reload(*args) |
@@ -121,7 +123,7 class User < Principal | |||
|
121 | 123 | return nil unless user.auth_source.authenticate(login, password) |
|
122 | 124 | else |
|
123 | 125 | # authentication with local password |
|
124 |
return nil unless |
|
|
126 | return nil unless user.check_password?(password) | |
|
125 | 127 | end |
|
126 | 128 | else |
|
127 | 129 | # user is not yet registered, try to authenticate with available sources |
@@ -200,13 +202,21 class User < Principal | |||
|
200 | 202 | update_attribute(:status, STATUS_LOCKED) |
|
201 | 203 | end |
|
202 | 204 | |
|
205 | # Returns true if +clear_password+ is the correct user's password, otherwise false | |
|
203 | 206 | def check_password?(clear_password) |
|
204 | 207 | if auth_source_id.present? |
|
205 | 208 | auth_source.authenticate(self.login, clear_password) |
|
206 | 209 | else |
|
207 |
User.hash_password(clear_password) == |
|
|
210 | User.hash_password("#{salt}#{User.hash_password clear_password}") == hashed_password | |
|
208 | 211 | end |
|
209 | 212 | end |
|
213 | ||
|
214 | # Generates a random salt and computes hashed_password for +clear_password+ | |
|
215 | # The hashed password is stored in the following form: SHA1(salt + SHA1(password)) | |
|
216 | def salt_password(clear_password) | |
|
217 | self.salt = User.generate_salt | |
|
218 | self.hashed_password = User.hash_password("#{salt}#{User.hash_password clear_password}") | |
|
219 | end | |
|
210 | 220 | |
|
211 | 221 | # Does the backend storage allow this user to change their password? |
|
212 | 222 | def change_password_allowed? |
@@ -473,6 +483,20 class User < Principal | |||
|
473 | 483 | end |
|
474 | 484 | anonymous_user |
|
475 | 485 | end |
|
486 | ||
|
487 | # Salts all existing unsalted passwords | |
|
488 | # It changes password storage scheme from SHA1(password) to SHA1(salt + SHA1(password)) | |
|
489 | # This method is used in the SaltPasswords migration and is to be kept as is | |
|
490 | def self.salt_unsalted_passwords! | |
|
491 | transaction do | |
|
492 | User.find_each(:conditions => "salt IS NULL OR salt = ''") do |user| | |
|
493 | next if user.hashed_password.blank? | |
|
494 | salt = User.generate_salt | |
|
495 | hashed_password = User.hash_password("#{salt}#{user.hashed_password}") | |
|
496 | User.update_all("salt = '#{salt}', hashed_password = '#{hashed_password}'", ["id = ?", user.id] ) | |
|
497 | end | |
|
498 | end | |
|
499 | end | |
|
476 | 500 | |
|
477 | 501 | protected |
|
478 | 502 | |
@@ -514,6 +538,12 class User < Principal | |||
|
514 | 538 | def self.hash_password(clear_password) |
|
515 | 539 | Digest::SHA1.hexdigest(clear_password || "") |
|
516 | 540 | end |
|
541 | ||
|
542 | # Returns a 128bits random salt as a hex string (32 chars long) | |
|
543 | def self.generate_salt | |
|
544 | ActiveSupport::SecureRandom.hex(16) | |
|
545 | end | |
|
546 | ||
|
517 | 547 | end |
|
518 | 548 | |
|
519 | 549 | class AnonymousUser < User |
@@ -148,7 +148,7 sub RedmineDSN { | |||
|
148 | 148 | my ($self, $parms, $arg) = @_; |
|
149 | 149 | $self->{RedmineDSN} = $arg; |
|
150 | 150 | my $query = "SELECT |
|
151 | hashed_password, auth_source_id, permissions | |
|
151 | hashed_password, salt, auth_source_id, permissions | |
|
152 | 152 | FROM members, projects, users, roles, member_roles |
|
153 | 153 | WHERE |
|
154 | 154 | projects.id=members.project_id |
@@ -316,11 +316,12 sub is_member { | |||
|
316 | 316 | $sth->execute($redmine_user, $project_id); |
|
317 | 317 | |
|
318 | 318 | my $ret; |
|
319 | while (my ($hashed_password, $auth_source_id, $permissions) = $sth->fetchrow_array) { | |
|
319 | while (my ($hashed_password, $salt, $auth_source_id, $permissions) = $sth->fetchrow_array) { | |
|
320 | 320 | |
|
321 | 321 | unless ($auth_source_id) { |
|
322 | my $method = $r->method; | |
|
323 | if ($hashed_password eq $pass_digest && ((defined $read_only_methods{$method} && $permissions =~ /:browse_repository/) || $permissions =~ /:commit_access/) ) { | |
|
322 | my $method = $r->method; | |
|
323 | my $salted_password = Digest::SHA1::sha1_hex($salt.$pass_digest); | |
|
324 | if ($hashed_password eq $salted_password && ((defined $read_only_methods{$method} && $permissions =~ /:browse_repository/) || $permissions =~ /:commit_access/) ) { | |
|
324 | 325 | $ret = 1; |
|
325 | 326 | last; |
|
326 | 327 | } |
@@ -4,7 +4,9 users_004: | |||
|
4 | 4 | status: 1 |
|
5 | 5 | last_login_on: |
|
6 | 6 | language: en |
|
7 | hashed_password: 4e4aeb7baaf0706bd670263fef42dad15763b608 | |
|
7 | # password = foo | |
|
8 | salt: 3126f764c3c5ac61cbfc103f25f934cf | |
|
9 | hashed_password: 9e4dd7eeb172c12a0691a6d9d3a269f7e9fe671b | |
|
8 | 10 | updated_on: 2006-07-19 19:34:07 +02:00 |
|
9 | 11 | admin: false |
|
10 | 12 | mail: rhill@somenet.foo |
@@ -20,7 +22,9 users_001: | |||
|
20 | 22 | status: 1 |
|
21 | 23 | last_login_on: 2006-07-19 22:57:52 +02:00 |
|
22 | 24 | language: en |
|
23 | hashed_password: d033e22ae348aeb5660fc2140aec35850c4da997 | |
|
25 | # password = admin | |
|
26 | salt: 82090c953c4a0000a7db253b0691a6b4 | |
|
27 | hashed_password: b5b6ff9543bf1387374cdfa27a54c96d236a7150 | |
|
24 | 28 | updated_on: 2006-07-19 22:57:52 +02:00 |
|
25 | 29 | admin: true |
|
26 | 30 | mail: admin@somenet.foo |
@@ -36,7 +40,9 users_002: | |||
|
36 | 40 | status: 1 |
|
37 | 41 | last_login_on: 2006-07-19 22:42:15 +02:00 |
|
38 | 42 | language: en |
|
39 | hashed_password: a9a653d4151fa2c081ba1ffc2c2726f3b80b7d7d | |
|
43 | # password = jsmith | |
|
44 | salt: 67eb4732624d5a7753dcea7ce0bb7d7d | |
|
45 | hashed_password: bfbe06043353a677d0215b26a5800d128d5413bc | |
|
40 | 46 | updated_on: 2006-07-19 22:42:15 +02:00 |
|
41 | 47 | admin: false |
|
42 | 48 | mail: jsmith@somenet.foo |
@@ -52,7 +58,9 users_003: | |||
|
52 | 58 | status: 1 |
|
53 | 59 | last_login_on: |
|
54 | 60 | language: en |
|
55 | hashed_password: 7feb7657aa7a7bf5aef3414a5084875f27192415 | |
|
61 | # password = foo | |
|
62 | salt: 7599f9963ec07b5a3b55b354407120c0 | |
|
63 | hashed_password: 8f659c8d7c072f189374edacfa90d6abbc26d8ed | |
|
56 | 64 | updated_on: 2006-07-19 19:33:19 +02:00 |
|
57 | 65 | admin: false |
|
58 | 66 | mail: dlopper@somenet.foo |
@@ -70,7 +78,7 users_005: | |||
|
70 | 78 | status: 3 |
|
71 | 79 | last_login_on: |
|
72 | 80 | language: en |
|
73 | hashed_password: 7feb7657aa7a7bf5aef3414a5084875f27192415 | |
|
81 | hashed_password: 1 | |
|
74 | 82 | updated_on: 2006-07-19 19:33:19 +02:00 |
|
75 | 83 | admin: false |
|
76 | 84 | mail: dlopper2@somenet.foo |
@@ -361,7 +361,6 class UserTest < ActiveSupport::TestCase | |||
|
361 | 361 | user = User.try_to_login("admin", "hello") |
|
362 | 362 | assert_kind_of User, user |
|
363 | 363 | assert_equal "admin", user.login |
|
364 | assert_equal User.hash_password("hello"), user.hashed_password | |
|
365 | 364 | end |
|
366 | 365 | |
|
367 | 366 | def test_name_format |
@@ -383,6 +382,22 class UserTest < ActiveSupport::TestCase | |||
|
383 | 382 | assert_equal nil, user |
|
384 | 383 | end |
|
385 | 384 | |
|
385 | context ".try_to_login" do | |
|
386 | context "with good credentials" do | |
|
387 | should "return the user" do | |
|
388 | user = User.try_to_login("admin", "admin") | |
|
389 | assert_kind_of User, user | |
|
390 | assert_equal "admin", user.login | |
|
391 | end | |
|
392 | end | |
|
393 | ||
|
394 | context "with wrong credentials" do | |
|
395 | should "return nil" do | |
|
396 | assert_nil User.try_to_login("admin", "foo") | |
|
397 | end | |
|
398 | end | |
|
399 | end | |
|
400 | ||
|
386 | 401 | if ldap_configured? |
|
387 | 402 | context "#try_to_login using LDAP" do |
|
388 | 403 | context "with failed connection to the LDAP server" do |
@@ -727,6 +742,23 class UserTest < ActiveSupport::TestCase | |||
|
727 | 742 | should 'be added and tested' |
|
728 | 743 | end |
|
729 | 744 | end |
|
745 | ||
|
746 | def test_salt_unsalted_passwords | |
|
747 | # Restore a user with an unsalted password | |
|
748 | user = User.find(1) | |
|
749 | user.salt = nil | |
|
750 | user.hashed_password = User.hash_password("unsalted") | |
|
751 | user.save! | |
|
752 | ||
|
753 | User.salt_unsalted_passwords! | |
|
754 | ||
|
755 | user.reload | |
|
756 | # Salt added | |
|
757 | assert !user.salt.blank? | |
|
758 | # Password still valid | |
|
759 | assert user.check_password?("unsalted") | |
|
760 | assert_equal user, User.try_to_login(user.login, "unsalted") | |
|
761 | end | |
|
730 | 762 | |
|
731 | 763 | if Object.const_defined?(:OpenID) |
|
732 | 764 |
General Comments 0
You need to be logged in to leave comments.
Login now