Class: Passlib::LdapDigest

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

Overview

Handles LDAP RFC 2307 digest password hashes.

Supports plain and salted variants for MD5, SHA-1, SHA-256, SHA-512, SHA3-256, and SHA3-512. MD5 and SHA-1 hashes may be stored in hex encoding (as produced by some LDAP implementations) or standard base64, both are detected automatically on load and preserved on round-trips.

Scheme names and their corresponding LDAP prefix:

  • MD5 / SMD5 (salted)

  • SHA / SSHA (salted)

  • SHA256 / SSHA256 (salted)

  • SHA512 / SSHA512 (salted)

  • SHA3-256 / SSHA3-256 (salted)

  • SHA3-512 / SSHA3-512 (salted)

Hash format: {SSHA512}base64data (or {MD5}hexdata for hex-encoded MD5/SHA)

Examples:

hash = Passlib::LdapDigest.create("hunter2", variant: "SSHA512")
hash.verify("hunter2")  # => true
hash.to_s               # => "{SSHA512}..."

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) ⇒ LdapDigest

Creates a new LDAP digest hash.

Parameters:

  • secret (String)

    the plaintext password

Options Hash (**options):

  • :variant (String, Symbol)

    LDAP scheme name — one of “MD5”, “SMD5”, “SHA”, “SSHA”, “SHA256”, “SSHA256”, “SHA512”, “SSHA512”, “SHA3-256”, “SSHA3-256”, “SHA3-512”, “SSHA3-512”; case-insensitive, symbols accepted, underscores may be used instead of dashes (default: “SSHA512”)

  • :salt (String)

    raw binary salt, only used for salted schemes (default: random, 4 bytes for MD5/SHA-1, 8 bytes for others)

  • :hex (Boolean)

    encode the output as hex instead of base64, only applicable to MD5 and SHA schemes (default: false)

Returns:



39
40
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
# File 'lib/passlib/ldap_digest.rb', line 39

class LdapDigest < Password
  SCHEMES = {
    "MD5"       => { digest: "MD5",      size: 16, salted: false },
    "SHA"       => { digest: "SHA1",     size: 20, salted: false },
    "SHA256"    => { digest: "SHA256",   size: 32, salted: false },
    "SHA512"    => { digest: "SHA512",   size: 64, salted: false },
    "SHA3-256"  => { digest: "SHA3-256", size: 32, salted: false },
    "SHA3-512"  => { digest: "SHA3-512", size: 64, salted: false },
    "SMD5"      => { digest: "MD5",      size: 16, salted: true, salt_size: 4 },
    "SSHA"      => { digest: "SHA1",     size: 20, salted: true, salt_size: 4 },
    "SSHA256"   => { digest: "SHA256",   size: 32, salted: true, salt_size: 8 },
    "SSHA512"   => { digest: "SHA512",   size: 64, salted: true, salt_size: 8 },
    "SSHA3-256" => { digest: "SHA3-256", size: 32, salted: true, salt_size: 8 },
    "SSHA3-512" => { digest: "SHA3-512", size: 64, salted: true, salt_size: 8 },
  }.freeze

  LOAD_PATTERN = /\A\{([A-Z0-9-]+)\}([A-Za-z0-9+\/=]+)\z/i
  DEFAULT      = "SSHA512"
  HEX_SCHEMES  = %w[MD5 SHA].freeze

  private_constant :SCHEMES, :LOAD_PATTERN, :DEFAULT, :HEX_SCHEMES

  # Register all scheme names (including hyphenated SHA3 variants) so the
  # updated LDAP auto-detection regex can match them directly.
  SCHEMES.each_key { Internal::Register::LDAP_IDS[_1] = self }
  register :ldap_digest

  options :variant, :salt, :hex

  # @param secret [String] the plaintext password to re-hash
  # @return [LdapDigest] a new instance hashed with the same scheme, salt, and encoding
  def create_comparable(secret) = self.class.create(secret, variant: @variant, salt: @salt, hex: @hex)

  def upgrade?
    configured = normalize_variant(config.variant)
    @variant != configured
  end

  private

  def normalize_variant(input)
    return DEFAULT unless input
    input.to_s.upcase.tr("_", "-")
  end

  def create(secret)
    @variant = normalize_variant(config.variant)
    scheme   = SCHEMES[@variant] or raise ArgumentError, "unknown LDAP scheme: #{@variant.inspect}"
    @salt    = config.salt || (scheme[:salted] ? Internal.random_bytes(scheme[:salt_size]) : nil)
    @hex     = config.hex
    checksum = OpenSSL::Digest.digest(scheme[:digest], secret.to_s.b + @salt.to_s.b)
    data     = scheme[:salted] ? checksum + @salt : checksum
    @hex ? "{#{@variant}}#{data.unpack1("H*")}" : "{#{@variant}}#{[data].pack("m0")}"
  end

  def load(string)
    m        = LOAD_PATTERN.match(string) or raise UnknownHashFormat, "invalid LDAP hash: #{string.inspect}"
    @variant = normalize_variant(m[1])
    scheme   = SCHEMES[@variant] or raise UnknownHashFormat, "unknown LDAP scheme: #{@variant.inspect}"
    @hex     = HEX_SCHEMES.include?(@variant) &&
               m[2].length == scheme[:size] * 2 &&
               m[2].match?(/\A[0-9a-f]+\z/i)
    raw      = @hex ? [m[2]].pack("H*") : m[2].unpack1("m")
    @salt    = raw[scheme[:size]..] if scheme[:salted]
    string
  end
end

Instance Method Details

#create_comparable(secret) ⇒ LdapDigest

Returns a new instance hashed with the same scheme, salt, and encoding.

Parameters:

  • secret (String)

    the plaintext password to re-hash

Returns:

  • (LdapDigest)

    a new instance hashed with the same scheme, salt, and encoding



70
# File 'lib/passlib/ldap_digest.rb', line 70

def create_comparable(secret) = self.class.create(secret, variant: @variant, salt: @salt, hex: @hex)

#upgrade?Boolean

Returns:

  • (Boolean)


72
73
74
75
# File 'lib/passlib/ldap_digest.rb', line 72

def upgrade?
  configured = normalize_variant(config.variant)
  @variant != configured
end