Skip to content

Commit a09652b

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

File tree

3 files changed

+126
-20
lines changed

3 files changed

+126
-20
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: 63 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,66 @@
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+
# since stretches and iterations are part of the hashed pass, so ignore them during comparing
10+
def self.compare(encrypted_password, password, _stretches, _salt, pepper)
11+
split_digest = self.split_digest(encrypted_password)
12+
value_to_test = sha512_checksum(password, split_digest[:stretches], split_digest[:salt], pepper)
13+
14+
Devise.secure_compare(split_digest[:checksum], value_to_test)
15+
end
16+
17+
def self.digest(password, stretches, salt, pepper)
18+
checksum = sha512_checksum(password, stretches, salt, pepper)
19+
20+
format_hash(STRATEGY, stretches, salt, checksum)
21+
end
22+
23+
private_class_method def self.sha512_checksum(password, stretches, salt, pepper)
24+
hash = OpenSSL::Digest.new('SHA512')
25+
pbkdf2_checksum(hash, password, stretches, salt, pepper)
26+
end
27+
28+
private_class_method def self.pbkdf2_checksum(hash, password, stretches, salt, pepper)
29+
OpenSSL::KDF.pbkdf2_hmac(
30+
password.to_s,
31+
salt: "#{[salt].pack('H*')}#{pepper}",
32+
iterations: stretches,
33+
hash: hash,
34+
length: hash.digest_length
35+
).unpack1('H*')
36+
end
37+
38+
# Passlib-style hash: $pbkdf2-sha512$rounds$salt$checksum
39+
# where salt and checksum are "adapted" Base64 encoded
40+
private_class_method def self.format_hash(strategy, stretches, salt, checksum)
41+
encoded_salt = passlib_encode64(salt)
42+
encoded_checksum = passlib_encode64(checksum)
43+
44+
"$#{strategy}$#{stretches}$#{encoded_salt}$#{encoded_checksum}"
45+
end
46+
47+
private_class_method def self.passlib_encode64(value)
48+
Base64.strict_encode64([value].pack('H*')).tr('+', '.').delete('=')
49+
end
50+
51+
private_class_method def self.passlib_decode64(value)
52+
enc = value.tr('.', '+')
53+
Base64.decode64(enc).unpack1('H*')
54+
end
55+
56+
private_class_method def self.split_digest(hash)
57+
split_digest = hash.split('$')
58+
_, strategy, stretches, salt, checksum = split_digest
59+
60+
raise InvalidHash, 'invalid PBKDF2 hash' unless split_digest.length == 5 && strategy.start_with?('pbkdf2-')
61+
62+
{ strategy: strategy, stretches: stretches.to_i,
63+
salt: passlib_decode64(salt), checksum: passlib_decode64(checksum) }
2164
end
2265
end
2366
end
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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+
STETCHES = 210_000
10+
PEPPER = 'thisisasuperlongstringusedontopofsalt'.freeze
11+
12+
def encrypt_password(admin, pepper = Admin.pepper, stretches = Admin.stretches, encryptor = Admin.encryptor_class)
13+
encryptor.digest('123456', stretches, admin.password_salt, pepper)
14+
end
15+
16+
def random_salt
17+
Devise::Encryptable::Encryptors::Base.salt(STETCHES)
18+
end
19+
20+
test 'digest and compare success' do
21+
plain_pass = 'password1'
22+
hashed_password = Devise::Encryptable::Encryptors::Pbkdf2.digest(plain_pass, STETCHES, random_salt, PEPPER)
23+
assert Devise::Encryptable::Encryptors::Pbkdf2.compare(hashed_password, plain_pass, nil, nil, PEPPER)
24+
end
25+
26+
test 'invalid password hash raise' do
27+
plain_pass = 'password1'
28+
assert_raise(Devise::Encryptable::Encryptors::InvalidHash) do
29+
Devise::Encryptable::Encryptors::Pbkdf2.compare('wrongformatpasshash', plain_pass, nil, nil, PEPPER)
30+
end
31+
end
32+
33+
test 'wrong password' do
34+
plain_pass = 'password1'
35+
hashed_password = Devise::Encryptable::Encryptors::Pbkdf2.digest(plain_pass, 210_000, random_salt, PEPPER)
36+
assert !Devise::Encryptable::Encryptors::Pbkdf2.compare(hashed_password, 'wrongpass', nil, nil, PEPPER)
37+
end
38+
39+
test 'changed pepper will fail password check' do
40+
plain_pass = 'password1'
41+
hashed_password = Devise::Encryptable::Encryptors::Pbkdf2.digest(plain_pass, 210_000, random_salt, PEPPER)
42+
assert !Devise::Encryptable::Encryptors::Pbkdf2.compare(hashed_password, plain_pass, nil, nil,
43+
'opps, different pepper')
44+
end
45+
46+
test 'devise using Pbkdf2' 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 using Pbkdf2' do
55+
swap_with_encryptor Admin, :Pbkdf2 do
56+
plain_pass = 'password1'
57+
admin = create_admin(password: plain_pass)
58+
assert admin.valid_password?(plain_pass)
59+
end
60+
end
61+
end

0 commit comments

Comments
 (0)