Class: Passlib::PBKDF2
Overview
Handles PBKDF2 password hashing via OpenSSL, in Passlib’s MCF format.
Five digest variants are available: SHA-1, SHA-256, SHA-512, SHA3-256, and SHA3-512. The SHA3 variants are only usable when the OpenSSL build supports them; the default is SHA3-512 when available, falling back to SHA-512.
Three hash formats are accepted on load, all normalized to MCF:
-
Passlib MCF: $pbkdf2-sha256$rounds$salt$dk
-
LDAP-style: {PBKDF2-SHA256}rounds$salt$dk
-
Cryptacular (
cta_pbkdf2_sha1): $p5k2$rounds_hex$salt_b64url$dk_b64url
New hashes are always produced in MCF.
Constant Summary
Constants included from Internal::DSL
Instance Attribute Summary
Attributes inherited from Password
Class Method Summary collapse
-
.create(secret, **options) ⇒ PBKDF2
Creates a new PBKDF2 hash.
Instance Method Summary collapse
-
#create_comparable(secret) ⇒ PBKDF2
A new instance hashed with the same variant, rounds, salt, and key length.
- #upgrade? ⇒ Boolean
Methods inherited from Password
available?, #initialize, #inspect, load, #pretty_print, #verify
Methods included from Internal::DSL
Constructor Details
This class inherits a constructor from Passlib::Password
Class Method Details
.create(secret, **options) ⇒ PBKDF2
Creates a new PBKDF2 hash.
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 |
# File 'lib/passlib/pbkdf2.rb', line 41 class PBKDF2 < Password VARIANTS = { "pbkdf2" => { digest: "SHA1", key_len: 20, default_rounds: 131_000 }, "pbkdf2-sha256" => { digest: "SHA256", key_len: 32, default_rounds: 29_000 }, "pbkdf2-sha512" => { digest: "SHA512", key_len: 64, default_rounds: 25_000 }, "pbkdf2-sha3-256" => { digest: "SHA3-256", key_len: 32, default_rounds: 29_000 }, "pbkdf2-sha3-512" => { digest: "SHA3-512", key_len: 64, default_rounds: 25_000 }, }.freeze # Mapping from LDAP-style variant names to their MCF equivalents. LDAP_VARIANTS = { "PBKDF2" => "pbkdf2", "PBKDF2-SHA256" => "pbkdf2-sha256", "PBKDF2-SHA512" => "pbkdf2-sha512", "PBKDF2-SHA3-256" => "pbkdf2-sha3-256", "PBKDF2-SHA3-512" => "pbkdf2-sha3-512", }.freeze # AB64 alphabet: A-Za-z0-9 plus '.' (replaces '+') and '/' MCF_PATTERN = /\A\$(pbkdf2(?:-sha3?-?\d+)?)\$(\d+)\$([A-Za-z0-9.\/]+)\$([A-Za-z0-9.\/]+)\z/ LDAP_PATTERN = /\A\{(PBKDF2[^}]*)\}(\d+)\$([A-Za-z0-9.\/]+)\$([A-Za-z0-9.\/]+)\z/i # Cryptacular's PBKDF2-SHA1 format: $p5k2$<rounds_hex>$<salt_b64url>$<checksum_b64url> CTA_PATTERN = %r{\A\$p5k2\$([0-9a-f]+)\$([A-Za-z0-9_=-]+)\$([A-Za-z0-9_=-]+)\z} DEFAULT = OpenSSL::Digest.digests.any? { it.casecmp?("SHA3-512") } ? "pbkdf2-sha3-512" : "pbkdf2-sha512" private_constant :VARIANTS, :LDAP_VARIANTS, :MCF_PATTERN, :LDAP_PATTERN, :CTA_PATTERN, :DEFAULT register mcf: %w[pbkdf2 p5k2] :variant, :rounds, :salt, :key_len # Register LDAP variant names for auto-detection via Passlib.load LDAP_VARIANTS.each_key { Internal::Register::LDAP_IDS[_1] = self } # @param secret [String] the plaintext password to re-hash # @return [PBKDF2] a new instance hashed with the same variant, rounds, salt, and key length def create_comparable(secret) self.class.create(secret, variant: @variant, rounds: @rounds, salt: @salt, key_len: @key_len) end def upgrade? v = VARIANTS[@variant] or return false @rounds != (config.rounds || v[:default_rounds]) end private def create(secret) @variant ||= (config.variant || DEFAULT).to_s.downcase.tr("_", "-") v = VARIANTS[@variant] or raise ArgumentError, "unknown pbkdf2 variant: #{@variant.inspect}" @rounds ||= config.rounds || v[:default_rounds] @key_len ||= config.key_len || v[:key_len] @salt ||= config.salt || Internal.random_bytes(16) dk = OpenSSL::PKCS5.pbkdf2_hmac(secret.to_s, @salt, @rounds, @key_len, v[:digest]) "$#{@variant}$#{@rounds}$#{Internal.encode_ab64(@salt)}$#{Internal.encode_ab64(dk)}" end def load(string) case string when MCF_PATTERN m = Regexp.last_match @variant = m[1] @rounds = m[2].to_i @salt = Internal.decode_ab64(m[3]) @key_len = Internal.decode_ab64(m[4]).bytesize string when LDAP_PATTERN m = Regexp.last_match ldap_var = m[1].upcase @variant = LDAP_VARIANTS[ldap_var] or raise UnknownHashFormat, "unknown LDAP PBKDF2 scheme: #{ldap_var.inspect}" @rounds = m[2].to_i @salt = Internal.decode_ab64(m[3]) @key_len = Internal.decode_ab64(m[4]).bytesize "$#{@variant}$#{@rounds}$#{m[3]}$#{m[4]}" when CTA_PATTERN m = Regexp.last_match @variant = "pbkdf2" @rounds = m[1].to_i(16) @salt = decode_url64(m[2]) @key_len = decode_url64(m[3]).bytesize "$pbkdf2$#{@rounds}$#{Internal.encode_ab64(@salt)}$#{m[3].then { Internal.encode_ab64(decode_url64(_1)) }}" else raise UnknownHashFormat, "invalid pbkdf2 hash: #{string.inspect}" end end def decode_url64(str) s = str.tr("-_", "+/") s += "=" * ((-s.length) % 4) unless s.end_with?("=") s.unpack1("m") end end |
Instance Method Details
#create_comparable(secret) ⇒ PBKDF2
Returns a new instance hashed with the same variant, rounds, salt, and key length.
77 78 79 |
# File 'lib/passlib/pbkdf2.rb', line 77 def create_comparable(secret) self.class.create(secret, variant: @variant, rounds: @rounds, salt: @salt, key_len: @key_len) end |
#upgrade? ⇒ Boolean
81 82 83 84 |
# File 'lib/passlib/pbkdf2.rb', line 81 def upgrade? v = VARIANTS[@variant] or return false @rounds != (config.rounds || v[:default_rounds]) end |