Skip to content

Commit eeceb56

Browse files
author
29decibel
committed
hash iterations into hashed pass
1 parent 82f56d9 commit eeceb56

File tree

4 files changed

+125
-21
lines changed

4 files changed

+125
-21
lines changed

lib/devise/encryptable/encryptable.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ module Devise
1919

2020
module Encryptable
2121
module Encryptors
22+
InvalidHash = Class.new(StandardError)
23+
2224
autoload :AuthlogicSha512, 'devise/encryptable/encryptors/authlogic_sha512'
2325
autoload :Base, 'devise/encryptable/encryptors/base'
2426
autoload :ClearanceSha1, 'devise/encryptable/encryptors/clearance_sha1'

lib/devise/encryptable/encryptors/pbkdf2.rb

Lines changed: 62 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,65 @@
1-
begin
2-
module Devise
3-
module Encryptable
4-
module Encryptors
5-
class Pbkdf2 < Base
6-
def self.compare(encrypted_password, password, stretches, salt, pepper)
7-
value_to_test = self.digest(password, stretches, salt, pepper)
8-
Devise.secure_compare(encrypted_password, value_to_test)
9-
end
10-
11-
def self.digest(password, stretches, salt, pepper)
12-
hash = OpenSSL::Digest.new('SHA512').new
13-
OpenSSL::KDF.pbkdf2_hmac(
14-
password.to_s,
15-
salt: "#{[salt].pack('H*')}#{pepper}",
16-
iterations: stretches,
17-
hash: hash,
18-
length: hash.digest_length,
19-
).unpack1('H*')
20-
end
1+
module Devise
2+
module Encryptable
3+
module Encryptors
4+
# https://en.wikipedia.org/wiki/PBKDF2
5+
# Adapted from https://gitlab.com/gitlab-org/gitlab/-/blob/373f088e755f678478b8dd1627fab908d2641b21/vendor/gems/devise-pbkdf2-encryptable/lib/devise/pbkdf2_encryptable/encryptors/pbkdf2_sha512.rb
6+
class Pbkdf2 < Base
7+
STRATEGY = 'pbkdf2-sha512'
8+
9+
def self.compare(encrypted_password, password, _stretches, _salt, pepper)
10+
split_digest = self.split_digest(encrypted_password)
11+
value_to_test = sha512_checksum(password, split_digest[:stretches], split_digest[:salt], pepper)
12+
13+
Devise.secure_compare(split_digest[:checksum], value_to_test)
14+
end
15+
16+
def self.digest(password, stretches, salt, pepper)
17+
checksum = sha512_checksum(password, stretches, salt, pepper)
18+
19+
format_hash(STRATEGY, stretches, salt, checksum)
20+
end
21+
22+
private_class_method def self.sha512_checksum(password, stretches, salt, pepper)
23+
hash = OpenSSL::Digest.new('SHA512')
24+
pbkdf2_checksum(hash, password, stretches, salt, pepper)
25+
end
26+
27+
private_class_method def self.pbkdf2_checksum(hash, password, stretches, salt, pepper)
28+
OpenSSL::KDF.pbkdf2_hmac(
29+
password.to_s,
30+
salt: "#{[salt].pack('H*')}#{pepper}",
31+
iterations: stretches,
32+
hash: hash,
33+
length: hash.digest_length
34+
).unpack1('H*')
35+
end
36+
37+
# Passlib-style hash: $pbkdf2-sha512$rounds$salt$checksum
38+
# where salt and checksum are "adapted" Base64 encoded
39+
private_class_method def self.format_hash(strategy, stretches, salt, checksum)
40+
encoded_salt = passlib_encode64(salt)
41+
encoded_checksum = passlib_encode64(checksum)
42+
43+
"$#{strategy}$#{stretches}$#{encoded_salt}$#{encoded_checksum}"
44+
end
45+
46+
private_class_method def self.passlib_encode64(value)
47+
Base64.strict_encode64([value].pack('H*')).tr('+', '.').delete('=')
48+
end
49+
50+
private_class_method def self.passlib_decode64(value)
51+
enc = value.tr('.', '+')
52+
Base64.decode64(enc).unpack1('H*')
53+
end
54+
55+
private_class_method def self.split_digest(hash)
56+
split_digest = hash.split('$')
57+
_, strategy, stretches, salt, checksum = split_digest
58+
59+
raise InvalidHash, 'invalid PBKDF2 hash' unless split_digest.length == 5 && strategy.start_with?('pbkdf2-')
60+
61+
{ strategy: strategy, stretches: stretches.to_i,
62+
salt: passlib_decode64(salt), checksum: passlib_decode64(checksum) }
2163
end
2264
end
2365
end
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
require 'test_helper'
2+
require 'benchmark'
3+
4+
class PBKDF2Test < ActiveSupport::TestCase
5+
include Support::Assertions
6+
include Support::Factories
7+
include Support::Swappers
8+
9+
def encrypt_password(admin, pepper = Admin.pepper, stretches = Admin.stretches, encryptor = Admin.encryptor_class)
10+
encryptor.digest('123456', stretches, admin.password_salt, pepper)
11+
end
12+
13+
test 'digest and compare' do
14+
aa = Benchmark.measure do
15+
pepper = 'hereis the long papeasdfasfasfasdfaf'
16+
hashed_password = Devise::Encryptable::Encryptors::Pbkdf2.digest('thisisalongpassthen', 210_000, 'sdfsfsf',
17+
pepper)
18+
assert Devise::Encryptable::Encryptors::Pbkdf2.compare(hashed_password, 'thisisalongpassthen', nil, nil, pepper)
19+
end
20+
puts aa
21+
end
22+
23+
test 'compare wrong format pass' do
24+
pepper = 'hereis the long papeasdfasfasfasdfaf'
25+
assert_raise(Devise::Encryptable::Encryptors::InvalidHash) do
26+
Devise::Encryptable::Encryptors::Pbkdf2.compare('wrong', 'thisisalongpassthen', nil, nil, pepper)
27+
end
28+
end
29+
30+
test 'wrong pass' do
31+
pepper = 'hereis the long papeasdfasfasfasdfaf'
32+
hashed_password = Devise::Encryptable::Encryptors::Pbkdf2.digest('thisisalongpassthen', 210_000, 'sdfsfsf',
33+
pepper)
34+
assert !Devise::Encryptable::Encryptors::Pbkdf2.compare(hashed_password, 'wrongpass', nil, nil, pepper)
35+
end
36+
37+
test 'pepper changed will fail password check' do
38+
pepper = 'hereis the long papeasdfasfasfasdfaf'
39+
plain_pass = 'thisis the plain pass'
40+
hashed_password = Devise::Encryptable::Encryptors::Pbkdf2.digest(plain_pass, 210_000, 'sdfsfsf',
41+
pepper)
42+
assert !Devise::Encryptable::Encryptors::Pbkdf2.compare(hashed_password, plain_pass, nil, nil,
43+
'opps, different pepper')
44+
end
45+
46+
test 'devise specifc going here' do
47+
swap_with_encryptor Admin, :Pbkdf2 do
48+
admin = create_admin
49+
assert_equal admin.encrypted_password,
50+
encrypt_password(admin, Admin.pepper, Admin.stretches, Devise::Encryptable::Encryptors::Pbkdf2)
51+
end
52+
end
53+
54+
test 'devise can compare right' do
55+
swap_with_encryptor Admin, :Pbkdf2 do
56+
admin = create_admin(password: 'nice')
57+
assert admin.valid_password?('nice')
58+
end
59+
end
60+
end

test/rails_app/config/initializers/devise.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
config.strip_whitespace_keys = [ :email ]
99
config.skip_session_storage = [:http_auth]
1010

11-
config.stretches = Rails.env.test? ? 1 : 10
11+
config.stretches = Rails.env.test? ? 1000 : 10
1212

1313
config.encryptor = :sha512
1414
end

0 commit comments

Comments
 (0)