devise-encryptable 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. data/.gitignore +17 -0
  2. data/.travis.yml +9 -0
  3. data/Changelog.md +4 -0
  4. data/Gemfile +16 -0
  5. data/Gemfile.lock +134 -0
  6. data/LICENSE +201 -0
  7. data/README.md +43 -0
  8. data/Rakefile +11 -0
  9. data/devise-encryptable.gemspec +19 -0
  10. data/gemfiles/Gemfile.rails-3.1.x +15 -0
  11. data/gemfiles/Gemfile.rails-3.1.x.lock +136 -0
  12. data/lib/devise-encryptable.rb +1 -0
  13. data/lib/devise/encryptable/encryptable.rb +28 -0
  14. data/lib/devise/encryptable/encryptors/authlogic_sha512.rb +21 -0
  15. data/lib/devise/encryptable/encryptors/base.rb +26 -0
  16. data/lib/devise/encryptable/encryptors/clearance_sha1.rb +19 -0
  17. data/lib/devise/encryptable/encryptors/restful_authentication_sha1.rb +24 -0
  18. data/lib/devise/encryptable/encryptors/sha1.rb +27 -0
  19. data/lib/devise/encryptable/encryptors/sha512.rb +27 -0
  20. data/lib/devise/encryptable/model.rb +86 -0
  21. data/lib/devise/encryptable/version.rb +5 -0
  22. data/test/devise/encryptable/encryptable_test.rb +65 -0
  23. data/test/devise/encryptable/encryptors_test.rb +32 -0
  24. data/test/rails_app/.gitignore +15 -0
  25. data/test/rails_app/Rakefile +7 -0
  26. data/test/rails_app/app/models/.gitkeep +0 -0
  27. data/test/rails_app/app/models/admin.rb +5 -0
  28. data/test/rails_app/app/models/user.rb +5 -0
  29. data/test/rails_app/config.ru +4 -0
  30. data/test/rails_app/config/application.rb +59 -0
  31. data/test/rails_app/config/boot.rb +6 -0
  32. data/test/rails_app/config/database.yml +3 -0
  33. data/test/rails_app/config/environment.rb +5 -0
  34. data/test/rails_app/config/environments/development.rb +37 -0
  35. data/test/rails_app/config/environments/production.rb +67 -0
  36. data/test/rails_app/config/environments/test.rb +37 -0
  37. data/test/rails_app/config/initializers/devise.rb +14 -0
  38. data/test/rails_app/db/migrate/20120508165529_create_tables.rb +77 -0
  39. data/test/support/assertions.rb +14 -0
  40. data/test/support/factories.rb +22 -0
  41. data/test/support/swappers.rb +28 -0
  42. data/test/test_helper.rb +21 -0
  43. metadata +126 -0
@@ -0,0 +1,15 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem "devise-encryptable", :path => ".."
4
+
5
+ gem 'devise', :git => "git://github.com/plataformatec/devise.git", :branch => "removing_encryptable"
6
+ gem 'minitest'
7
+ gem 'rails', "3.1.4"
8
+ gem 'sqlite3'
9
+
10
+ gem 'mocha', :require => false
11
+
12
+ gem 'pry'
13
+ gem 'pry-doc'
14
+ gem 'pry-nav'
15
+ gem 'awesome_print'
@@ -0,0 +1,136 @@
1
+ GIT
2
+ remote: git://github.com/plataformatec/devise.git
3
+ revision: 0d868b9ec1e0c7d3a041bb65c2889b5bd23d65dc
4
+ branch: removing_encryptable
5
+ specs:
6
+ devise (2.1.0.rc)
7
+ bcrypt-ruby (~> 3.0)
8
+ orm_adapter (~> 0.0.7)
9
+ railties (~> 3.1)
10
+ warden (~> 1.1.1)
11
+
12
+ PATH
13
+ remote: ..
14
+ specs:
15
+ devise-encryptable (0.0.1)
16
+ devise (~> 2.1.0.rc)
17
+
18
+ GEM
19
+ remote: https://rubygems.org/
20
+ specs:
21
+ actionmailer (3.1.4)
22
+ actionpack (= 3.1.4)
23
+ mail (~> 2.3.0)
24
+ actionpack (3.1.4)
25
+ activemodel (= 3.1.4)
26
+ activesupport (= 3.1.4)
27
+ builder (~> 3.0.0)
28
+ erubis (~> 2.7.0)
29
+ i18n (~> 0.6)
30
+ rack (~> 1.3.6)
31
+ rack-cache (~> 1.1)
32
+ rack-mount (~> 0.8.2)
33
+ rack-test (~> 0.6.1)
34
+ sprockets (~> 2.0.3)
35
+ activemodel (3.1.4)
36
+ activesupport (= 3.1.4)
37
+ builder (~> 3.0.0)
38
+ i18n (~> 0.6)
39
+ activerecord (3.1.4)
40
+ activemodel (= 3.1.4)
41
+ activesupport (= 3.1.4)
42
+ arel (~> 2.2.3)
43
+ tzinfo (~> 0.3.29)
44
+ activeresource (3.1.4)
45
+ activemodel (= 3.1.4)
46
+ activesupport (= 3.1.4)
47
+ activesupport (3.1.4)
48
+ multi_json (~> 1.0)
49
+ arel (2.2.3)
50
+ awesome_print (1.0.2)
51
+ bcrypt-ruby (3.0.1)
52
+ builder (3.0.0)
53
+ coderay (1.0.6)
54
+ erubis (2.7.0)
55
+ hike (1.2.1)
56
+ i18n (0.6.0)
57
+ json (1.7.1)
58
+ mail (2.3.3)
59
+ i18n (>= 0.4.0)
60
+ mime-types (~> 1.16)
61
+ treetop (~> 1.4.8)
62
+ metaclass (0.0.1)
63
+ method_source (0.7.1)
64
+ mime-types (1.18)
65
+ minitest (3.0.0)
66
+ mocha (0.11.4)
67
+ metaclass (~> 0.0.1)
68
+ multi_json (1.3.4)
69
+ orm_adapter (0.0.7)
70
+ polyglot (0.3.3)
71
+ pry (0.9.9.4)
72
+ coderay (~> 1.0.5)
73
+ method_source (~> 0.7.1)
74
+ slop (>= 2.4.4, < 3)
75
+ pry-doc (0.4.1)
76
+ pry (>= 0.9.0)
77
+ yard (~> 0.7.4)
78
+ pry-nav (0.2.1)
79
+ pry (~> 0.9.9)
80
+ rack (1.3.6)
81
+ rack-cache (1.2)
82
+ rack (>= 0.4)
83
+ rack-mount (0.8.3)
84
+ rack (>= 1.0.0)
85
+ rack-ssl (1.3.2)
86
+ rack
87
+ rack-test (0.6.1)
88
+ rack (>= 1.0)
89
+ rails (3.1.4)
90
+ actionmailer (= 3.1.4)
91
+ actionpack (= 3.1.4)
92
+ activerecord (= 3.1.4)
93
+ activeresource (= 3.1.4)
94
+ activesupport (= 3.1.4)
95
+ bundler (~> 1.0)
96
+ railties (= 3.1.4)
97
+ railties (3.1.4)
98
+ actionpack (= 3.1.4)
99
+ activesupport (= 3.1.4)
100
+ rack-ssl (~> 1.3.2)
101
+ rake (>= 0.8.7)
102
+ rdoc (~> 3.4)
103
+ thor (~> 0.14.6)
104
+ rake (0.9.2.2)
105
+ rdoc (3.12)
106
+ json (~> 1.4)
107
+ slop (2.4.4)
108
+ sprockets (2.0.4)
109
+ hike (~> 1.2)
110
+ rack (~> 1.0)
111
+ tilt (~> 1.1, != 1.3.0)
112
+ sqlite3 (1.3.6)
113
+ thor (0.14.6)
114
+ tilt (1.3.3)
115
+ treetop (1.4.10)
116
+ polyglot
117
+ polyglot (>= 0.3.1)
118
+ tzinfo (0.3.33)
119
+ warden (1.1.1)
120
+ rack (>= 1.0)
121
+ yard (0.7.5)
122
+
123
+ PLATFORMS
124
+ ruby
125
+
126
+ DEPENDENCIES
127
+ awesome_print
128
+ devise!
129
+ devise-encryptable!
130
+ minitest
131
+ mocha
132
+ pry
133
+ pry-doc
134
+ pry-nav
135
+ rails (= 3.1.4)
136
+ sqlite3
@@ -0,0 +1 @@
1
+ require "devise/encryptable/encryptable"
@@ -0,0 +1,28 @@
1
+ module Devise
2
+
3
+ # Declare encryptors length which are used in migrations.
4
+ ENCRYPTORS_LENGTH = {
5
+ :sha1 => 40,
6
+ :sha512 => 128,
7
+ :clearance_sha1 => 40,
8
+ :restful_authentication_sha1 => 40,
9
+ :authlogic_sha512 => 128
10
+ }
11
+
12
+ # Used to define the password encryption algorithm.
13
+ mattr_accessor :encryptor
14
+ @@encryptor = nil
15
+
16
+ module Encryptable
17
+ module Encryptors
18
+ autoload :AuthlogicSha512, 'devise/encryptable/encryptors/authlogic_sha512'
19
+ autoload :Base, 'devise/encryptable/encryptors/base'
20
+ autoload :ClearanceSha1, 'devise/encryptable/encryptors/clearance_sha1'
21
+ autoload :RestfulAuthenticationSha1, 'devise/encryptable/encryptors/restful_authentication_sha1'
22
+ autoload :Sha1, 'devise/encryptable/encryptors/sha1'
23
+ autoload :Sha512, 'devise/encryptable/encryptors/sha512'
24
+ end
25
+ end
26
+ end
27
+
28
+ Devise.add_module(:encryptable, :model => 'devise/encryptable/model')
@@ -0,0 +1,21 @@
1
+ require "digest/sha2"
2
+
3
+ module Devise
4
+ module Encryptable
5
+ module Encryptors
6
+ # = AuthlogicSha512
7
+ # Simulates Authlogic's default encryption mechanism.
8
+ # Warning: it uses Devise's stretches configuration to port Authlogic's one. Should be set to 20 in the initializer to simulate
9
+ # the default behavior.
10
+ class AuthlogicSha512 < Base
11
+ # Generates a default password digest based on salt, pepper and the
12
+ # incoming password.
13
+ def self.digest(password, stretches, salt, pepper)
14
+ digest = [password, salt].flatten.join('')
15
+ stretches.times { digest = Digest::SHA512.hexdigest(digest) }
16
+ digest
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,26 @@
1
+ module Devise
2
+ # Implements a way of adding different encryptions.
3
+ # The class should implement a self.digest method that taks the following params:
4
+ # - password
5
+ # - stretches: the number of times the encryption will be applied
6
+ # - salt: the password salt as defined by devise
7
+ # - pepper: Devise config option
8
+ #
9
+ module Encryptable
10
+ module Encryptors
11
+ class Base
12
+ def self.digest
13
+ raise NotImplemented
14
+ end
15
+
16
+ def self.salt(stretches)
17
+ Devise.friendly_token[0,20]
18
+ end
19
+
20
+ def self.compare(encrypted_password, password, stretches, salt, pepper)
21
+ Devise.secure_compare(encrypted_password, digest(password, stretches, salt, pepper))
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,19 @@
1
+ require "digest/sha1"
2
+
3
+ module Devise
4
+ module Encryptable
5
+ module Encryptors
6
+ # = ClearanceSha1
7
+ # Simulates Clearance's default encryption mechanism.
8
+ # Warning: it uses Devise's pepper to port the concept of REST_AUTH_SITE_KEY
9
+ # Warning: it uses Devise's stretches configuration to port the concept of REST_AUTH_DIGEST_STRETCHES
10
+ class ClearanceSha1 < Base
11
+ # Generates a default password digest based on salt, pepper and the
12
+ # incoming password.
13
+ def self.digest(password, stretches, salt, pepper)
14
+ Digest::SHA1.hexdigest("--#{salt}--#{password}--")
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,24 @@
1
+ require "digest/sha1"
2
+
3
+ module Devise
4
+ module Encryptable
5
+ module Encryptors
6
+ # = RestfulAuthenticationSha1
7
+ # Simulates Restful Authentication's default encryption mechanism.
8
+ # Warning: it uses Devise's pepper to port the concept of REST_AUTH_SITE_KEY
9
+ # Warning: it uses Devise's stretches configuration to port the concept of REST_AUTH_DIGEST_STRETCHES. Should be set to 10 in
10
+ # the initializer to simulate the default behavior.
11
+ class RestfulAuthenticationSha1 < Base
12
+
13
+ # Generates a default password digest based on salt, pepper and the
14
+ # incoming password.
15
+ def self.digest(password, stretches, salt, pepper)
16
+ digest = pepper
17
+ stretches.times { digest = Digest::SHA1.hexdigest([digest, salt, password, pepper].flatten.join('--')) }
18
+ digest
19
+ end
20
+
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,27 @@
1
+ require "digest/sha1"
2
+
3
+ module Devise
4
+ module Encryptable
5
+ module Encryptors
6
+ # = Sha1
7
+ # Uses the Sha1 hash algorithm to encrypt passwords.
8
+ class Sha1 < Base
9
+ # Generates a default password digest based on stretches, salt, pepper and the
10
+ # incoming password.
11
+ def self.digest(password, stretches, salt, pepper)
12
+ digest = pepper
13
+ stretches.times { digest = self.secure_digest(salt, digest, password, pepper) }
14
+ digest
15
+ end
16
+
17
+ private
18
+
19
+ # Generate a SHA1 digest joining args. Generated token is something like
20
+ # --arg1--arg2--arg3--argN--
21
+ def self.secure_digest(*tokens)
22
+ ::Digest::SHA1.hexdigest('--' << tokens.flatten.join('--') << '--')
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,27 @@
1
+ require "digest/sha2"
2
+
3
+ module Devise
4
+ module Encryptable
5
+ module Encryptors
6
+ # = Sha512
7
+ # Uses the Sha512 hash algorithm to encrypt passwords.
8
+ class Sha512 < Base
9
+ # Generates a default password digest based on salt, pepper and the
10
+ # incoming password.
11
+ def self.digest(password, stretches, salt, pepper)
12
+ digest = pepper
13
+ stretches.times { digest = self.secure_digest(salt, digest, password, pepper) }
14
+ digest
15
+ end
16
+
17
+ private
18
+
19
+ # Generate a Sha512 digest joining args. Generated token is something like
20
+ # --arg1--arg2--arg3--argN--
21
+ def self.secure_digest(*tokens)
22
+ ::Digest::SHA512.hexdigest('--' << tokens.flatten.join('--') << '--')
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,86 @@
1
+ require 'devise/strategies/database_authenticatable'
2
+
3
+ module Devise
4
+ module Models
5
+ # Encryptable module adds support to several encryptors wrapping
6
+ # them in a salt and pepper mechanism to increase security.
7
+ #
8
+ # == Options
9
+ #
10
+ # Encryptable adds the following options to devise_for:
11
+ #
12
+ # * +pepper+: a random string used to provide a more secure hash.
13
+ #
14
+ # * +encryptor+: the encryptor going to be used. By default is nil.
15
+ #
16
+ # == Examples
17
+ #
18
+ # User.find(1).valid_password?('password123') # returns true/false
19
+ #
20
+ module Encryptable
21
+ extend ActiveSupport::Concern
22
+
23
+ included do
24
+ attr_reader :password, :current_password
25
+ attr_accessor :password_confirmation
26
+ end
27
+
28
+ def self.required_fields(klass)
29
+ [:password_salt]
30
+ end
31
+
32
+ # Generates password salt when setting the password.
33
+ def password=(new_password)
34
+ self.password_salt = self.class.password_salt if new_password.present?
35
+ super
36
+ end
37
+
38
+ # Validates the password considering the salt.
39
+ def valid_password?(password)
40
+ return false if encrypted_password.blank?
41
+ encryptor_class.compare(encrypted_password, password, self.class.stretches, authenticatable_salt, self.class.pepper)
42
+ end
43
+
44
+ # Overrides authenticatable salt to use the new password_salt
45
+ # column. authenticatable_salt is used by `valid_password?`
46
+ # and by other modules whenever there is a need for a random
47
+ # token based on the user password.
48
+ def authenticatable_salt
49
+ self.password_salt
50
+ end
51
+
52
+ protected
53
+
54
+ # Digests the password using the configured encryptor.
55
+ def password_digest(password)
56
+ if password_salt.present?
57
+ encryptor_class.digest(password, self.class.stretches, authenticatable_salt, self.class.pepper)
58
+ end
59
+ end
60
+
61
+ def encryptor_class
62
+ self.class.encryptor_class
63
+ end
64
+
65
+ module ClassMethods
66
+ Devise::Models.config(self, :encryptor)
67
+
68
+ # Returns the class for the configured encryptor.
69
+ def encryptor_class
70
+ @encryptor_class ||= case encryptor
71
+ when :bcrypt
72
+ raise "In order to use bcrypt as encryptor, simply remove :encryptable from your devise model"
73
+ when nil
74
+ raise "You need to give an :encryptor as option in order to use :encryptable"
75
+ else
76
+ Devise::Encryptable::Encryptors.const_get(encryptor.to_s.classify)
77
+ end
78
+ end
79
+
80
+ def password_salt
81
+ self.encryptor_class.salt(self.stretches)
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,5 @@
1
+ module Devise
2
+ module Encryptable
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
@@ -0,0 +1,65 @@
1
+ require "test_helper"
2
+
3
+ class EncryptableTest < ActiveSupport::TestCase
4
+ include Support::Assertions
5
+ include Support::Factories
6
+ include Support::Swappers
7
+
8
+ def encrypt_password(admin, pepper=Admin.pepper, stretches=Admin.stretches, encryptor=Admin.encryptor_class)
9
+ encryptor.digest('123456', stretches, admin.password_salt, pepper)
10
+ end
11
+
12
+ test 'should generate salt while setting password' do
13
+ assert_present create_admin.password_salt
14
+ end
15
+
16
+ test 'should not change password salt when updating' do
17
+ admin = create_admin
18
+ salt = admin.password_salt
19
+ admin.expects(:password_salt=).never
20
+ admin.save!
21
+ assert_equal salt, admin.password_salt
22
+ end
23
+
24
+ test 'should generate a base64 hash using SecureRandom for password salt' do
25
+ swap_with_encryptor Admin, :sha1 do
26
+ SecureRandom.expects(:base64).with(15).returns('01lI').once
27
+ salt = create_admin.password_salt
28
+ assert_not_equal '01lI', salt
29
+ assert_equal 4, salt.size
30
+ end
31
+ end
32
+
33
+ test 'should not generate salt if password is blank' do
34
+ assert_blank create_admin(:password => nil).password_salt
35
+ assert_blank create_admin(:password => '').password_salt
36
+ end
37
+
38
+ test 'should encrypt password again if password has changed' do
39
+ admin = create_admin
40
+ encrypted_password = admin.encrypted_password
41
+ admin.password = admin.password_confirmation = 'new_password'
42
+ admin.save!
43
+ assert_not_equal encrypted_password, admin.encrypted_password
44
+ end
45
+
46
+ test 'should respect encryptor configuration' do
47
+ swap_with_encryptor Admin, :sha512 do
48
+ admin = create_admin
49
+ assert_equal admin.encrypted_password, encrypt_password(admin, Admin.pepper, Admin.stretches, Devise::Encryptable::Encryptors::Sha512)
50
+ end
51
+ end
52
+
53
+ test 'should not validate password when salt is nil' do
54
+ admin = create_admin
55
+ admin.password_salt = nil
56
+ admin.save
57
+ assert_not admin.valid_password?('123456')
58
+ end
59
+
60
+ test 'required_fields should contain the fields that Devise uses' do
61
+ assert_same_content Devise::Models::Encryptable.required_fields(Admin), [
62
+ :password_salt
63
+ ]
64
+ end
65
+ end