Skip to content

Commit 254bf8b

Browse files
author
Mike Li
authored
GFSB-1389 validating encrypted_password using given encryptor strategy with fallbacks (#5)
1 parent 655fc67 commit 254bf8b

File tree

8 files changed

+147
-197
lines changed

8 files changed

+147
-197
lines changed

.github/workflows/test.yml

Lines changed: 13 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -1,130 +1,21 @@
11
name: Test
22
on: [push, pull_request]
3+
34
jobs:
45
test:
6+
runs-on: ubuntu-latest
57
strategy:
6-
fail-fast: false
78
matrix:
8-
gemfile:
9-
- Gemfile
10-
- gemfiles/Gemfile-rails-main
11-
- gemfiles/Gemfile-rails-6-1
12-
- gemfiles/Gemfile-rails-6-0
13-
- gemfiles/Gemfile-rails-5-2
14-
- gemfiles/Gemfile-rails-5-1
15-
- gemfiles/Gemfile-rails-5-0
16-
- gemfiles/Gemfile-rails-4-2
17-
- gemfiles/Gemfile-rails-4-1
18-
ruby:
19-
- '3.1'
20-
- '3.0'
21-
- '2.7'
22-
- '2.6'
23-
- '2.5'
24-
- '2.4'
25-
- '2.3'
26-
- '2.2'
27-
- '2.1'
28-
exclude:
29-
- gemfile: Gemfile
30-
ruby: '2.6'
31-
- gemfile: Gemfile
32-
ruby: '2.5'
33-
- gemfile: Gemfile
34-
ruby: '2.4'
35-
- gemfile: Gemfile
36-
ruby: '2.3'
37-
- gemfile: Gemfile
38-
ruby: '2.2'
39-
- gemfile: Gemfile
40-
ruby: '2.1'
41-
- gemfile: gemfiles/Gemfile-rails-main
42-
ruby: '2.6'
43-
- gemfile: gemfiles/Gemfile-rails-main
44-
ruby: '2.5'
45-
- gemfile: gemfiles/Gemfile-rails-main
46-
ruby: '2.4'
47-
- gemfile: gemfiles/Gemfile-rails-main
48-
ruby: '2.3'
49-
- gemfile: gemfiles/Gemfile-rails-main
50-
ruby: '2.2'
51-
- gemfile: gemfiles/Gemfile-rails-main
52-
ruby: '2.1'
53-
- gemfile: gemfiles/Gemfile-rails-6-1
54-
ruby: '2.4'
55-
- gemfile: gemfiles/Gemfile-rails-6-1
56-
ruby: '2.3'
57-
- gemfile: gemfiles/Gemfile-rails-6-1
58-
ruby: '2.2'
59-
- gemfile: gemfiles/Gemfile-rails-6-1
60-
ruby: '2.1'
61-
- gemfile: gemfiles/Gemfile-rails-6-0
62-
ruby: '3.1'
63-
- gemfile: gemfiles/Gemfile-rails-6-0
64-
ruby: '2.4'
65-
- gemfile: gemfiles/Gemfile-rails-6-0
66-
ruby: '2.3'
67-
- gemfile: gemfiles/Gemfile-rails-6-0
68-
ruby: '2.2'
69-
- gemfile: gemfiles/Gemfile-rails-6-0
70-
ruby: '2.1'
71-
- gemfile: gemfiles/Gemfile-rails-5-2
72-
ruby: '3.1'
73-
- gemfile: gemfiles/Gemfile-rails-5-2
74-
ruby: '3.0'
75-
- gemfile: gemfiles/Gemfile-rails-5-2
76-
ruby: '2.7'
77-
- gemfile: gemfiles/Gemfile-rails-5-2
78-
ruby: '2.2'
79-
- gemfile: gemfiles/Gemfile-rails-5-2
80-
ruby: '2.1'
81-
- gemfile: gemfiles/Gemfile-rails-5-1
82-
ruby: '3.1'
83-
- gemfile: gemfiles/Gemfile-rails-5-1
84-
ruby: '3.0'
85-
- gemfile: gemfiles/Gemfile-rails-5-1
86-
ruby: '2.7'
87-
- gemfile: gemfiles/Gemfile-rails-5-1
88-
ruby: '2.1'
89-
- gemfile: gemfiles/Gemfile-rails-5-0
90-
ruby: '3.1'
91-
- gemfile: gemfiles/Gemfile-rails-5-0
92-
ruby: '3.0'
93-
- gemfile: gemfiles/Gemfile-rails-5-0
94-
ruby: '2.7'
95-
- gemfile: gemfiles/Gemfile-rails-5-0
96-
ruby: '2.1'
97-
- gemfile: gemfiles/Gemfile-rails-4-2
98-
ruby: '3.1'
99-
- gemfile: gemfiles/Gemfile-rails-4-2
100-
ruby: '3.0'
101-
- gemfile: gemfiles/Gemfile-rails-4-2
102-
ruby: '2.7'
103-
- gemfile: gemfiles/Gemfile-rails-4-2
104-
ruby: '2.6'
105-
- gemfile: gemfiles/Gemfile-rails-4-1
106-
ruby: '3.1'
107-
- gemfile: gemfiles/Gemfile-rails-4-1
108-
ruby: '3.0'
109-
- gemfile: gemfiles/Gemfile-rails-4-1
110-
ruby: '2.7'
111-
- gemfile: gemfiles/Gemfile-rails-4-1
112-
ruby: '2.6'
113-
- gemfile: gemfiles/Gemfile-rails-4-1
114-
ruby: '2.5'
115-
- gemfile: gemfiles/Gemfile-rails-4-1
116-
ruby: '2.4'
117-
runs-on: ubuntu-latest
118-
env: # $BUNDLE_GEMFILE must be set at the job level, so it is set for all steps
119-
BUNDLE_GEMFILE: ${{ matrix.gemfile }}
9+
ruby-version: ['3.2', '3.1']
10+
12011
steps:
121-
- uses: actions/checkout@v2
122-
- name: Setup Bundler 1.x for Rails 4.x
123-
if: ${{ matrix.gemfile == 'gemfiles/Gemfile-rails-4-1' || matrix.gemfile == 'gemfiles/Gemfile-rails-4-2' }}
124-
run: echo "BUNDLER_VERSION=1.17.3" >> $GITHUB_ENV
125-
- uses: ruby/setup-ruby@v1
12+
- uses: actions/checkout@v3
13+
- name: Set up Ruby ${{ matrix.ruby-version }}
14+
uses: ruby/setup-ruby@v1
12615
with:
127-
ruby-version: ${{ matrix.ruby }}
128-
bundler-cache: true # runs bundle install and caches installed gems automatically
129-
bundler: ${{ env.BUNDLER_VERSION || 'latest' }}
130-
- run: bundle exec rake
16+
ruby-version: ${{ matrix.ruby-version }}
17+
bundler-cache: true
18+
- name: Install dependencies
19+
run: bundle install
20+
- name: Run tests
21+
run: bundle exec rake

Gemfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,5 @@ gem 'rails', '~> 7.0.0'
77
gem 'sqlite3'
88

99
gem 'mocha', '~> 1.0', require: false
10+
11+
gem 'simplecov', require: false, group: :test

Gemfile.lock

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ GEM
8383
responders
8484
warden (~> 1.2.3)
8585
digest (3.1.0)
86+
docile (1.4.0)
8687
erubi (1.10.0)
8788
globalid (1.0.0)
8889
activesupport (>= 5.0)
@@ -154,6 +155,12 @@ GEM
154155
responders (3.0.1)
155156
actionpack (>= 5.0)
156157
railties (>= 5.0)
158+
simplecov (0.22.0)
159+
docile (~> 1.1)
160+
simplecov-html (~> 0.11)
161+
simplecov_json_formatter (~> 0.1)
162+
simplecov-html (0.12.3)
163+
simplecov_json_formatter (0.1.4)
157164
sqlite3 (1.4.2)
158165
strscan (3.0.1)
159166
thor (1.2.1)
@@ -175,6 +182,7 @@ DEPENDENCIES
175182
devise-encryptable!
176183
mocha (~> 1.0)
177184
rails (~> 7.0.0)
185+
simplecov
178186
sqlite3
179187

180188
BUNDLED WITH

lib/devise/migratable/encryptors/pbkdf2_sha512.rb

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,13 @@ def self.digest(password, stretches, salt, pepper)
1919

2020
format_hash(STRATEGY, stretches, salt, checksum)
2121
end
22+
23+
def self.valid_hash?(pass)
24+
split_digest(pass)
25+
true
26+
rescue StandardError
27+
false
28+
end
2229

2330
private_class_method def self.sha512_checksum(password, stretches, salt, pepper)
2431
hash = OpenSSL::Digest.new('SHA512')
@@ -66,4 +73,4 @@ def self.digest(password, stretches, salt, pepper)
6673
end
6774
end
6875
end
69-
76+

lib/devise/migratable/migratable.rb

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,20 @@ module Devise
33
mattr_accessor :encryptor
44
@@encryptor = nil
55

6-
# block to be evaluated to determine wether to validate using encryptor
7-
mattr_accessor :enable_validation
8-
@@enable_validation = nil
6+
mattr_accessor :override_existing_password_hash
7+
@@override_existing_password_hash = nil
98

10-
# Sets enable_validation block
9+
10+
# Sets override_existing_password_hash block
1111
#
1212
# Devise.setup do |config|
1313
#
14-
# config.validate_using_encryptor do |user|
15-
# Features.active?(:enable_pbkdf2_validation, user)
14+
# config.override_existing_password_hash_check do |user|
15+
# Features.active?(:override_existing_password_hash_using_encryptor, user)
1616
# end
1717
# end
18-
def self.validate_using_encryptor(&block)
19-
@@enable_validation = block
18+
def self.override_existing_password_hash_check(&block)
19+
@@override_existing_password_hash = block
2020
end
2121

2222
module Migratable

lib/devise/migratable/model.rb

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
require 'devise/strategies/database_authenticatable'
2+
require 'bcrypt'
23

34
module Devise
45
module Models
@@ -50,26 +51,29 @@ def self.required_fields(_klass)
5051
# Generates new password for the new encryptor
5152
# generate password of the old one using super
5253
def password=(new_password)
53-
# everytime we store the new one with the given encryptor
54-
self.encrypted_password_migrate_to = generate_digest_for_password(new_password)
55-
# this will set the original password to the database, nothing changed
54+
unless override_existing_password_hash_enabled?
55+
self.encrypted_password_migrate_to = generate_digest_for_password(new_password)
56+
end
5657
super
5758
end
5859

5960
# Validates the password considering the salt.
6061
def valid_password?(password)
6162
return false if encrypted_password.blank?
6263

63-
# only if feature enabled? then we do the work here
64-
if encryptor_validation_enabled? && encrypted_password_migrate_to.present?
64+
if valid_encryptor_hash?
6565
valid_password_using_encryptor?(password)
6666
else
6767
result = super
68-
update_encrypted_password_migrate_to(password) if result && encrypted_password_migrate_to.nil?
68+
update_encrypted_password_hash(password) if result
6969
result
7070
end
7171
end
7272

73+
def valid_encryptor_hash?
74+
encryptor_class.valid_hash?(encrypted_password)
75+
end
76+
7377
# redefine serializable_hash to prevent encrypted_password_migrate_to leaking
7478
def serializable_hash(options = {})
7579
options[:except] ||= []
@@ -79,12 +83,17 @@ def serializable_hash(options = {})
7983

8084
protected
8185

82-
def update_encrypted_password_migrate_to(password)
86+
def password_digest(password)
87+
override_existing_password_hash_enabled? ? generate_digest_for_password(password) : super
88+
end
89+
90+
def update_encrypted_password_hash(password)
8391
return if new_record?
8492

85-
update_column(:encrypted_password_migrate_to, generate_digest_for_password(password))
93+
column_name = override_existing_password_hash_enabled? ? :encrypted_password : :encrypted_password_migrate_to
94+
update_column(column_name, generate_digest_for_password(password))
8695
rescue StandardError => e # capture StandardError instead of ActiveRecordError to play safe
87-
log_error('Failed to update_column encrypted_password_migrate_to', e)
96+
log_error("Failed to update_column #{column_name}", e)
8897
end
8998

9099
def generate_digest_for_password(password)
@@ -94,13 +103,13 @@ def generate_digest_for_password(password)
94103
self.class.pepper)
95104
end
96105

97-
def encryptor_validation_enabled?
98-
self.class.enable_validation&.call(self)
106+
def override_existing_password_hash_enabled?
107+
self.class.override_existing_password_hash&.call(self)
99108
end
100109

101110
def valid_password_using_encryptor?(password)
102111
encryptor_arguments = [
103-
encrypted_password_migrate_to,
112+
encrypted_password,
104113
password,
105114
self.class.stretches,
106115
self.class.password_salt,
@@ -119,7 +128,7 @@ def encryptor_class
119128

120129
# class method get injected into Devise module
121130
module ClassMethods
122-
Devise::Models.config(self, :encryptor, :enable_validation)
131+
Devise::Models.config(self, :encryptor, :override_existing_password_hash)
123132

124133
# Returns the class for the configured encryptor.
125134
def encryptor_class
@@ -135,9 +144,9 @@ def password_salt
135144
def compute_encryptor_class(encryptor)
136145
case encryptor
137146
when :bcrypt
138-
raise 'In order to use bcrypt as encryptor, simply remove :encryptable from your devise model'
147+
raise 'In order to use bcrypt as encryptor, simply remove :migratable from your devise model'
139148
when nil
140-
raise 'You need to give an :encryptor as option in order to use :encryptable'
149+
raise 'You need to give an :encryptor as option in order to use :migratable'
141150
else
142151
Devise::Migratable::Encryptors.const_get(encryptor.to_s.classify)
143152
end

0 commit comments

Comments
 (0)