diff --git a/app/models/user.rb b/app/models/user.rb index 0e170ee..6107d07 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -52,18 +52,63 @@ class User < ActiveRecord::Base u && u.authenticated?(password) ? u : nil end - # Encrypts some data with the salt. - def self.encrypt(password, salt) - Digest::SHA1.hexdigest("--#{salt}--#{password}--") + class UnsupportedEncryptionType < Exception; end + + def self.system_encryption_method + @system_encryption_method || :salted_sha1 + end + + def self.system_encryption_method=(method) + @system_encryption_method = method + end + + # a Hash containing the available encryption methods. Keys are symbols, + # values are Proc objects that contain the actual encryption code. + def self.encryption_methods + @encryption_methods ||= {} + end + + # adds a new encryption method. + def self.add_encryption_method(sym, &block) + encryption_methods[sym] = block + end + + # the encryption method used for this instance + def encryption_method + (password_type || User.system_encryption_method).to_sym end - # Encrypts the password with the user salt + # Encrypts the password using the chosen method def encrypt(password) - self.class.encrypt(password, salt) + method = self.class.encryption_methods[encryption_method] + if method + method.call(password, salt) + else + raise UnsupportedEncryptionType, "Unsupported encryption type: #{encryption_method}" + end + end + + add_encryption_method :salted_sha1 do |password, salt| + Digest::SHA1.hexdigest("--#{salt}--#{password}--") + end + + add_encryption_method :md5 do |password, salt| + Digest::MD5.hexdigest(password) + end + + add_encryption_method :clear do |password, salt| + password end def authenticated?(password) - crypted_password == encrypt(password) + result = (crypted_password == encrypt(password)) + if (encryption_method != User.system_encryption_method) && result + self.password_type = User.system_encryption_method.to_s + self.password = password + self.password_confirmation = password + self.save! + end + result end def remember_token? @@ -120,6 +165,7 @@ class User < ActiveRecord::Base def encrypt_password return if password.blank? self.salt = Digest::SHA1.hexdigest("--#{Time.now.to_s}--#{login}--") if new_record? + self.password_type ||= User.system_encryption_method.to_s self.crypted_password = encrypt(password) end diff --git a/test/unit/user_test.rb b/test/unit/user_test.rb index a56f607..126224c 100644 --- a/test/unit/user_test.rb +++ b/test/unit/user_test.rb @@ -162,6 +162,95 @@ class UserTest < Test::Unit::TestCase assert !Person.find_by_identifier('lalala') end + def test_should_encrypt_password_with_salted_sha1 + user = User.new(:login => 'lalala', :email => 'lalala@example.com', :password => 'test', :password_confirmation => 'test') + user.expects(:salt).returns('testsalt') + user.save! + + # SHA1+salt crypted form for password 'test', and salt 'testsalt', + # calculated by hand at IRB + crypted_password = '77606e8e9227f73618eefdfd36f8eb1b8b52ca5f' + + assert_equal crypted_password, user.crypted_password + end + + def test_should_support_md5_passwords + # ATTENTION this test explicitly exposes the crypted form of 'test'. This + # makes 'test' a terrible password. :) + user = create_user(:login => 'lalala', :email => 'lalala@example.com', :password => 'test', :password_confirmation => 'test', :password_type => 'md5') + assert_equal '098f6bcd4621d373cade4e832627b4f6', user.crypted_password + end + + def test_should_support_clear_passwords + assert_equal 'test', create_user(:password => 'test', :password_confirmation => 'test', :password_type => 'clear').crypted_password + end + + def test_should_only_allow_know_encryption_methods + assert_raise User::UnsupportedEncryptionType do + User.create( + :login => 'lalala', + :email => 'lalala@example.com', + :password => 'test', + :password_confirmation => 'test', + :password_type => 'AN_ENCRYPTION_METHOD_NOT_LIKELY_TO_EXIST' # <<<< + ) + end + end + + def test_should_use_salted_sha1_by_default + assert_equal :salted_sha1, User.system_encryption_method + end + + def test_should_be_able_to_set_system_encryption_method + # save + saved = User.system_encryption_method + + User.system_encryption_method = :some_method + assert_equal :some_method, User.system_encryption_method + + # restore + User.system_encryption_method = saved + end + + def test_new_instances_should_use_system_encryption_method + User.expects(:system_encryption_method).returns(:clear) + assert_equal 'clear', create_user.password_type + end + + def test_should_reencrypt_password_when_using_different_encryption_method_from_the_system_default + User.stubs(:system_encryption_method).returns(:salted_sha1) + + # a user was created ... + user = create_user(:login => 'lalala', :email => 'lalala@example.com', :password => 'test', :password_confirmation => 'test', :password_type => 'salted_sha1') + + # then the sysadmin decided to change the encryption method + User.expects(:system_encryption_method).returns(:md5).at_least_once + + # when the user logs in, her password must be reencrypted with the new + # method + user.authenticated?('test') + + # and the new password must be saved back to the database + user.reload + assert_equal '098f6bcd4621d373cade4e832627b4f6', user.crypted_password + end + + def test_should_not_update_encryption_if_password_incorrect + # a user was created + User.stubs(:system_encryption_method).returns(:salted_sha1) + user = create_user(:login => 'lalala', :email => 'lalala@example.com', :password => 'test', :password_confirmation => 'test', :password_type => 'salted_sha1') + crypted_password = user.crypted_password + + # then the sysadmin deciced to change the encryption method + User.expects(:system_encryption_method).returns(:md5).at_least_once + + # but the user provided the wrong password + user.authenticated?('WRONG_PASSWORD') + + # and then her password is not updated + assert_equal crypted_password, user.crypted_password + end + protected def create_user(options = {}) User.create({ :login => 'quire', :email => 'quire@example.com', :password => 'quire', :password_confirmation => 'quire' }.merge(options)) -- libgit2 0.21.2