Commit 74d6f92d54feaf59525695b650197903e9fe5724
1 parent
b4d9cf76
Exists in
master
and in
29 other branches
ActionItem44: changing user to accomodate different types of encryption
for password git-svn-id: https://svn.colivre.coop.br/svn/noosfero/trunk@1849 3f533792-8f58-4932-b0fe-aaf55b0a4547
Showing
2 changed files
with
141 additions
and
6 deletions
Show diff stats
app/models/user.rb
@@ -52,18 +52,63 @@ class User < ActiveRecord::Base | @@ -52,18 +52,63 @@ class User < ActiveRecord::Base | ||
52 | u && u.authenticated?(password) ? u : nil | 52 | u && u.authenticated?(password) ? u : nil |
53 | end | 53 | end |
54 | 54 | ||
55 | - # Encrypts some data with the salt. | ||
56 | - def self.encrypt(password, salt) | ||
57 | - Digest::SHA1.hexdigest("--#{salt}--#{password}--") | 55 | + class UnsupportedEncryptionType < Exception; end |
56 | + | ||
57 | + def self.system_encryption_method | ||
58 | + @system_encryption_method || :salted_sha1 | ||
59 | + end | ||
60 | + | ||
61 | + def self.system_encryption_method=(method) | ||
62 | + @system_encryption_method = method | ||
63 | + end | ||
64 | + | ||
65 | + # a Hash containing the available encryption methods. Keys are symbols, | ||
66 | + # values are Proc objects that contain the actual encryption code. | ||
67 | + def self.encryption_methods | ||
68 | + @encryption_methods ||= {} | ||
69 | + end | ||
70 | + | ||
71 | + # adds a new encryption method. | ||
72 | + def self.add_encryption_method(sym, &block) | ||
73 | + encryption_methods[sym] = block | ||
74 | + end | ||
75 | + | ||
76 | + # the encryption method used for this instance | ||
77 | + def encryption_method | ||
78 | + (password_type || User.system_encryption_method).to_sym | ||
58 | end | 79 | end |
59 | 80 | ||
60 | - # Encrypts the password with the user salt | 81 | + # Encrypts the password using the chosen method |
61 | def encrypt(password) | 82 | def encrypt(password) |
62 | - self.class.encrypt(password, salt) | 83 | + method = self.class.encryption_methods[encryption_method] |
84 | + if method | ||
85 | + method.call(password, salt) | ||
86 | + else | ||
87 | + raise UnsupportedEncryptionType, "Unsupported encryption type: #{encryption_method}" | ||
88 | + end | ||
89 | + end | ||
90 | + | ||
91 | + add_encryption_method :salted_sha1 do |password, salt| | ||
92 | + Digest::SHA1.hexdigest("--#{salt}--#{password}--") | ||
93 | + end | ||
94 | + | ||
95 | + add_encryption_method :md5 do |password, salt| | ||
96 | + Digest::MD5.hexdigest(password) | ||
97 | + end | ||
98 | + | ||
99 | + add_encryption_method :clear do |password, salt| | ||
100 | + password | ||
63 | end | 101 | end |
64 | 102 | ||
65 | def authenticated?(password) | 103 | def authenticated?(password) |
66 | - crypted_password == encrypt(password) | 104 | + result = (crypted_password == encrypt(password)) |
105 | + if (encryption_method != User.system_encryption_method) && result | ||
106 | + self.password_type = User.system_encryption_method.to_s | ||
107 | + self.password = password | ||
108 | + self.password_confirmation = password | ||
109 | + self.save! | ||
110 | + end | ||
111 | + result | ||
67 | end | 112 | end |
68 | 113 | ||
69 | def remember_token? | 114 | def remember_token? |
@@ -120,6 +165,7 @@ class User < ActiveRecord::Base | @@ -120,6 +165,7 @@ class User < ActiveRecord::Base | ||
120 | def encrypt_password | 165 | def encrypt_password |
121 | return if password.blank? | 166 | return if password.blank? |
122 | self.salt = Digest::SHA1.hexdigest("--#{Time.now.to_s}--#{login}--") if new_record? | 167 | self.salt = Digest::SHA1.hexdigest("--#{Time.now.to_s}--#{login}--") if new_record? |
168 | + self.password_type ||= User.system_encryption_method.to_s | ||
123 | self.crypted_password = encrypt(password) | 169 | self.crypted_password = encrypt(password) |
124 | end | 170 | end |
125 | 171 |
test/unit/user_test.rb
@@ -162,6 +162,95 @@ class UserTest < Test::Unit::TestCase | @@ -162,6 +162,95 @@ class UserTest < Test::Unit::TestCase | ||
162 | assert !Person.find_by_identifier('lalala') | 162 | assert !Person.find_by_identifier('lalala') |
163 | end | 163 | end |
164 | 164 | ||
165 | + def test_should_encrypt_password_with_salted_sha1 | ||
166 | + user = User.new(:login => 'lalala', :email => 'lalala@example.com', :password => 'test', :password_confirmation => 'test') | ||
167 | + user.expects(:salt).returns('testsalt') | ||
168 | + user.save! | ||
169 | + | ||
170 | + # SHA1+salt crypted form for password 'test', and salt 'testsalt', | ||
171 | + # calculated by hand at IRB | ||
172 | + crypted_password = '77606e8e9227f73618eefdfd36f8eb1b8b52ca5f' | ||
173 | + | ||
174 | + assert_equal crypted_password, user.crypted_password | ||
175 | + end | ||
176 | + | ||
177 | + def test_should_support_md5_passwords | ||
178 | + # ATTENTION this test explicitly exposes the crypted form of 'test'. This | ||
179 | + # makes 'test' a terrible password. :) | ||
180 | + user = create_user(:login => 'lalala', :email => 'lalala@example.com', :password => 'test', :password_confirmation => 'test', :password_type => 'md5') | ||
181 | + assert_equal '098f6bcd4621d373cade4e832627b4f6', user.crypted_password | ||
182 | + end | ||
183 | + | ||
184 | + def test_should_support_clear_passwords | ||
185 | + assert_equal 'test', create_user(:password => 'test', :password_confirmation => 'test', :password_type => 'clear').crypted_password | ||
186 | + end | ||
187 | + | ||
188 | + def test_should_only_allow_know_encryption_methods | ||
189 | + assert_raise User::UnsupportedEncryptionType do | ||
190 | + User.create( | ||
191 | + :login => 'lalala', | ||
192 | + :email => 'lalala@example.com', | ||
193 | + :password => 'test', | ||
194 | + :password_confirmation => 'test', | ||
195 | + :password_type => 'AN_ENCRYPTION_METHOD_NOT_LIKELY_TO_EXIST' # <<<< | ||
196 | + ) | ||
197 | + end | ||
198 | + end | ||
199 | + | ||
200 | + def test_should_use_salted_sha1_by_default | ||
201 | + assert_equal :salted_sha1, User.system_encryption_method | ||
202 | + end | ||
203 | + | ||
204 | + def test_should_be_able_to_set_system_encryption_method | ||
205 | + # save | ||
206 | + saved = User.system_encryption_method | ||
207 | + | ||
208 | + User.system_encryption_method = :some_method | ||
209 | + assert_equal :some_method, User.system_encryption_method | ||
210 | + | ||
211 | + # restore | ||
212 | + User.system_encryption_method = saved | ||
213 | + end | ||
214 | + | ||
215 | + def test_new_instances_should_use_system_encryption_method | ||
216 | + User.expects(:system_encryption_method).returns(:clear) | ||
217 | + assert_equal 'clear', create_user.password_type | ||
218 | + end | ||
219 | + | ||
220 | + def test_should_reencrypt_password_when_using_different_encryption_method_from_the_system_default | ||
221 | + User.stubs(:system_encryption_method).returns(:salted_sha1) | ||
222 | + | ||
223 | + # a user was created ... | ||
224 | + user = create_user(:login => 'lalala', :email => 'lalala@example.com', :password => 'test', :password_confirmation => 'test', :password_type => 'salted_sha1') | ||
225 | + | ||
226 | + # then the sysadmin decided to change the encryption method | ||
227 | + User.expects(:system_encryption_method).returns(:md5).at_least_once | ||
228 | + | ||
229 | + # when the user logs in, her password must be reencrypted with the new | ||
230 | + # method | ||
231 | + user.authenticated?('test') | ||
232 | + | ||
233 | + # and the new password must be saved back to the database | ||
234 | + user.reload | ||
235 | + assert_equal '098f6bcd4621d373cade4e832627b4f6', user.crypted_password | ||
236 | + end | ||
237 | + | ||
238 | + def test_should_not_update_encryption_if_password_incorrect | ||
239 | + # a user was created | ||
240 | + User.stubs(:system_encryption_method).returns(:salted_sha1) | ||
241 | + user = create_user(:login => 'lalala', :email => 'lalala@example.com', :password => 'test', :password_confirmation => 'test', :password_type => 'salted_sha1') | ||
242 | + crypted_password = user.crypted_password | ||
243 | + | ||
244 | + # then the sysadmin deciced to change the encryption method | ||
245 | + User.expects(:system_encryption_method).returns(:md5).at_least_once | ||
246 | + | ||
247 | + # but the user provided the wrong password | ||
248 | + user.authenticated?('WRONG_PASSWORD') | ||
249 | + | ||
250 | + # and then her password is not updated | ||
251 | + assert_equal crypted_password, user.crypted_password | ||
252 | + end | ||
253 | + | ||
165 | protected | 254 | protected |
166 | def create_user(options = {}) | 255 | def create_user(options = {}) |
167 | User.create({ :login => 'quire', :email => 'quire@example.com', :password => 'quire', :password_confirmation => 'quire' }.merge(options)) | 256 | User.create({ :login => 'quire', :email => 'quire@example.com', :password => 'quire', :password_confirmation => 'quire' }.merge(options)) |