##// END OF EJS Templates
Adds random salt to user passwords (#7410)....
Jean-Philippe Lang -
r4816:ce84bb1a0194
parent child
Show More
@@ -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 self.hashed_password = User.hash_password(self.password) if self.password && self.auth_source_id.blank?
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 User.hash_password(password) == user.hashed_password
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) == self.hashed_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