|
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) } |
21 | 64 | end |
22 | 65 | end |
23 | 66 | end |
|
0 commit comments