@@ -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 | def before_save |
|
84 | def before_save | |
85 | # update hashed_password if password was set |
|
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 | end |
|
89 | end | |
88 |
|
90 | |||
89 | def reload(*args) |
|
91 | def reload(*args) | |
@@ -121,7 +123,7 class User < Principal | |||||
121 | return nil unless user.auth_source.authenticate(login, password) |
|
123 | return nil unless user.auth_source.authenticate(login, password) | |
122 | else |
|
124 | else | |
123 | # authentication with local password |
|
125 | # authentication with local password | |
124 |
return nil unless |
|
126 | return nil unless user.check_password?(password) | |
125 | end |
|
127 | end | |
126 | else |
|
128 | else | |
127 | # user is not yet registered, try to authenticate with available sources |
|
129 | # user is not yet registered, try to authenticate with available sources | |
@@ -200,13 +202,21 class User < Principal | |||||
200 | update_attribute(:status, STATUS_LOCKED) |
|
202 | update_attribute(:status, STATUS_LOCKED) | |
201 | end |
|
203 | end | |
202 |
|
204 | |||
|
205 | # Returns true if +clear_password+ is the correct user's password, otherwise false | |||
203 | def check_password?(clear_password) |
|
206 | def check_password?(clear_password) | |
204 | if auth_source_id.present? |
|
207 | if auth_source_id.present? | |
205 | auth_source.authenticate(self.login, clear_password) |
|
208 | auth_source.authenticate(self.login, clear_password) | |
206 | else |
|
209 | else | |
207 |
User.hash_password(clear_password) == |
|
210 | User.hash_password("#{salt}#{User.hash_password clear_password}") == hashed_password | |
208 | end |
|
211 | end | |
209 | end |
|
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 | # Does the backend storage allow this user to change their password? |
|
221 | # Does the backend storage allow this user to change their password? | |
212 | def change_password_allowed? |
|
222 | def change_password_allowed? | |
@@ -473,6 +483,20 class User < Principal | |||||
473 | end |
|
483 | end | |
474 | anonymous_user |
|
484 | anonymous_user | |
475 | end |
|
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 | protected |
|
501 | protected | |
478 |
|
502 | |||
@@ -514,6 +538,12 class User < Principal | |||||
514 | def self.hash_password(clear_password) |
|
538 | def self.hash_password(clear_password) | |
515 | Digest::SHA1.hexdigest(clear_password || "") |
|
539 | Digest::SHA1.hexdigest(clear_password || "") | |
516 | end |
|
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 | end |
|
547 | end | |
518 |
|
548 | |||
519 | class AnonymousUser < User |
|
549 | class AnonymousUser < User |
@@ -148,7 +148,7 sub RedmineDSN { | |||||
148 | my ($self, $parms, $arg) = @_; |
|
148 | my ($self, $parms, $arg) = @_; | |
149 | $self->{RedmineDSN} = $arg; |
|
149 | $self->{RedmineDSN} = $arg; | |
150 | my $query = "SELECT |
|
150 | my $query = "SELECT | |
151 | hashed_password, auth_source_id, permissions |
|
151 | hashed_password, salt, auth_source_id, permissions | |
152 | FROM members, projects, users, roles, member_roles |
|
152 | FROM members, projects, users, roles, member_roles | |
153 | WHERE |
|
153 | WHERE | |
154 | projects.id=members.project_id |
|
154 | projects.id=members.project_id | |
@@ -316,11 +316,12 sub is_member { | |||||
316 | $sth->execute($redmine_user, $project_id); |
|
316 | $sth->execute($redmine_user, $project_id); | |
317 |
|
317 | |||
318 | my $ret; |
|
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 | unless ($auth_source_id) { |
|
321 | unless ($auth_source_id) { | |
322 | my $method = $r->method; |
|
322 | my $method = $r->method; | |
323 | if ($hashed_password eq $pass_digest && ((defined $read_only_methods{$method} && $permissions =~ /:browse_repository/) || $permissions =~ /:commit_access/) ) { |
|
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 | $ret = 1; |
|
325 | $ret = 1; | |
325 | last; |
|
326 | last; | |
326 | } |
|
327 | } |
@@ -4,7 +4,9 users_004: | |||||
4 | status: 1 |
|
4 | status: 1 | |
5 | last_login_on: |
|
5 | last_login_on: | |
6 | language: en |
|
6 | language: en | |
7 | hashed_password: 4e4aeb7baaf0706bd670263fef42dad15763b608 |
|
7 | # password = foo | |
|
8 | salt: 3126f764c3c5ac61cbfc103f25f934cf | |||
|
9 | hashed_password: 9e4dd7eeb172c12a0691a6d9d3a269f7e9fe671b | |||
8 | updated_on: 2006-07-19 19:34:07 +02:00 |
|
10 | updated_on: 2006-07-19 19:34:07 +02:00 | |
9 | admin: false |
|
11 | admin: false | |
10 | mail: rhill@somenet.foo |
|
12 | mail: rhill@somenet.foo | |
@@ -20,7 +22,9 users_001: | |||||
20 | status: 1 |
|
22 | status: 1 | |
21 | last_login_on: 2006-07-19 22:57:52 +02:00 |
|
23 | last_login_on: 2006-07-19 22:57:52 +02:00 | |
22 | language: en |
|
24 | language: en | |
23 | hashed_password: d033e22ae348aeb5660fc2140aec35850c4da997 |
|
25 | # password = admin | |
|
26 | salt: 82090c953c4a0000a7db253b0691a6b4 | |||
|
27 | hashed_password: b5b6ff9543bf1387374cdfa27a54c96d236a7150 | |||
24 | updated_on: 2006-07-19 22:57:52 +02:00 |
|
28 | updated_on: 2006-07-19 22:57:52 +02:00 | |
25 | admin: true |
|
29 | admin: true | |
26 | mail: admin@somenet.foo |
|
30 | mail: admin@somenet.foo | |
@@ -36,7 +40,9 users_002: | |||||
36 | status: 1 |
|
40 | status: 1 | |
37 | last_login_on: 2006-07-19 22:42:15 +02:00 |
|
41 | last_login_on: 2006-07-19 22:42:15 +02:00 | |
38 | language: en |
|
42 | language: en | |
39 | hashed_password: a9a653d4151fa2c081ba1ffc2c2726f3b80b7d7d |
|
43 | # password = jsmith | |
|
44 | salt: 67eb4732624d5a7753dcea7ce0bb7d7d | |||
|
45 | hashed_password: bfbe06043353a677d0215b26a5800d128d5413bc | |||
40 | updated_on: 2006-07-19 22:42:15 +02:00 |
|
46 | updated_on: 2006-07-19 22:42:15 +02:00 | |
41 | admin: false |
|
47 | admin: false | |
42 | mail: jsmith@somenet.foo |
|
48 | mail: jsmith@somenet.foo | |
@@ -52,7 +58,9 users_003: | |||||
52 | status: 1 |
|
58 | status: 1 | |
53 | last_login_on: |
|
59 | last_login_on: | |
54 | language: en |
|
60 | language: en | |
55 | hashed_password: 7feb7657aa7a7bf5aef3414a5084875f27192415 |
|
61 | # password = foo | |
|
62 | salt: 7599f9963ec07b5a3b55b354407120c0 | |||
|
63 | hashed_password: 8f659c8d7c072f189374edacfa90d6abbc26d8ed | |||
56 | updated_on: 2006-07-19 19:33:19 +02:00 |
|
64 | updated_on: 2006-07-19 19:33:19 +02:00 | |
57 | admin: false |
|
65 | admin: false | |
58 | mail: dlopper@somenet.foo |
|
66 | mail: dlopper@somenet.foo | |
@@ -70,7 +78,7 users_005: | |||||
70 | status: 3 |
|
78 | status: 3 | |
71 | last_login_on: |
|
79 | last_login_on: | |
72 | language: en |
|
80 | language: en | |
73 | hashed_password: 7feb7657aa7a7bf5aef3414a5084875f27192415 |
|
81 | hashed_password: 1 | |
74 | updated_on: 2006-07-19 19:33:19 +02:00 |
|
82 | updated_on: 2006-07-19 19:33:19 +02:00 | |
75 | admin: false |
|
83 | admin: false | |
76 | mail: dlopper2@somenet.foo |
|
84 | mail: dlopper2@somenet.foo |
@@ -361,7 +361,6 class UserTest < ActiveSupport::TestCase | |||||
361 | user = User.try_to_login("admin", "hello") |
|
361 | user = User.try_to_login("admin", "hello") | |
362 | assert_kind_of User, user |
|
362 | assert_kind_of User, user | |
363 | assert_equal "admin", user.login |
|
363 | assert_equal "admin", user.login | |
364 | assert_equal User.hash_password("hello"), user.hashed_password |
|
|||
365 | end |
|
364 | end | |
366 |
|
365 | |||
367 | def test_name_format |
|
366 | def test_name_format | |
@@ -383,6 +382,22 class UserTest < ActiveSupport::TestCase | |||||
383 | assert_equal nil, user |
|
382 | assert_equal nil, user | |
384 | end |
|
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 | if ldap_configured? |
|
401 | if ldap_configured? | |
387 | context "#try_to_login using LDAP" do |
|
402 | context "#try_to_login using LDAP" do | |
388 | context "with failed connection to the LDAP server" do |
|
403 | context "with failed connection to the LDAP server" do | |
@@ -727,6 +742,23 class UserTest < ActiveSupport::TestCase | |||||
727 | should 'be added and tested' |
|
742 | should 'be added and tested' | |
728 | end |
|
743 | end | |
729 | end |
|
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 | if Object.const_defined?(:OpenID) |
|
763 | if Object.const_defined?(:OpenID) | |
732 |
|
764 |
General Comments 0
You need to be logged in to leave comments.
Login now