Class: Passlib::PBKDF2

Inherits:
Password show all
Defined in:
lib/passlib/pbkdf2.rb

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.

Examples:

hash = Passlib::PBKDF2.create("hunter2", variant: "pbkdf2-sha256", rounds: 29_000)
hash.verify("hunter2")  # => true
hash.to_s               # => "$pbkdf2-sha256$29000$...$..."

Loading an LDAP hash (normalized to MCF)

hash = Passlib::PBKDF2.load("{PBKDF2-SHA256}29000$...$...")
hash.to_s               # => "$pbkdf2-sha256$29000$...$..."

Loading a Cryptacular cta_pbkdf2_sha1 hash (normalized to MCF)

hash = Passlib::PBKDF2.load("$p5k2$2710$...$...")
hash.to_s               # => "$pbkdf2$10000$...$..."

Constant Summary

Constants included from Internal::DSL

Internal::DSL::Config

Instance Attribute Summary

Attributes inherited from Password

#config, #string

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Password

available?, #initialize, #inspect, load, #pretty_print, #verify

Methods included from Internal::DSL

#identifier

Constructor Details

This class inherits a constructor from Passlib::Password

Class Method Details

.create(secret, **options) ⇒ PBKDF2

Creates a new PBKDF2 hash.

Parameters:

  • secret (String)

    the plaintext password

Options Hash (**options):

  • :variant (String, Symbol)

    digest variant — one of “pbkdf2” (SHA-1), “pbkdf2-sha256”, “pbkdf2-sha512”, “pbkdf2-sha3-256”, “pbkdf2-sha3-512”; case-insensitive, symbols accepted, underscores may be used instead of dashes (default: “pbkdf2-sha3-512” if SHA3 is available, otherwise “pbkdf2-sha512”)

  • :rounds (Integer)

    iteration count (default: variant-specific)

  • :salt (String)

    raw binary salt (default: 16 random bytes)

  • :key_len (Integer)

    derived key length in bytes (default: variant-specific)

Returns:



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]
  options :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.

Parameters:

  • secret (String)

    the plaintext password to re-hash

Returns:

  • (PBKDF2)

    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

Returns:

  • (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