devise-encryptable 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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