123456789_123456789_123456789_123456789_123456789_

Class: Net::IMAP::SASL::DigestMD5Authenticator

Relationships & Source Files
Inherits: Object
Defined in: lib/net/imap/sasl/digest_md5_authenticator.rb

Overview

::Net::IMAP authenticator for the DIGEST-MD5 ::Net::IMAP::SASL mechanism type, specified in RFC-2831. See Net::IMAP#authenticate.

Deprecated

DIGEST-MD5” has been deprecated by RFC-6331 and should not be relied on for security. It is included for compatibility with existing servers.

Constant Summary

Class Method Summary

Instance Attribute Summary

Instance Method Summary

Constructor Details

.new(username, password, authzid = nil, **options) ⇒ authenticator .new(username:, password:, authzid: nil, **options) ⇒ authenticator .new(authcid:, password:, authzid: nil, **options) ⇒ authenticator

Creates an Authenticator for the “DIGEST-MD5::Net::IMAP::SASL mechanism.

Called by Net::IMAP#authenticate and similar methods on other clients.

Parameters

  • #authcid ― Authentication identity that is associated with #password.

    #username ― An alias for #authcid.

  • #password ― A password or passphrase associated with this #authcid.

  • optional #authzid ― Authorization identity to act as or on behalf of.

    When #authzid is not set, the server should derive the authorization identity from the authentication identity.

  • optional #realm — A namespace for the #username, e.g. a domain. Defaults to the last realm in the server-provided realms list.

  • optional #host — FQDN for requested service. Defaults to #realm.

  • optional #service_name — The generic host name when the server is replicated.

  • optional #service — the registered service protocol. E.g. “imap”, “smtp”, “ldap”, “xmpp”. For Net::IMAP, this defaults to “imap”.

  • optional warn_deprecation — Set to false to silence the warning.

Any other keyword arguments are silently ignored.

[ GitHub ]

  
# File 'lib/net/imap/sasl/digest_md5_authenticator.rb', line 154

def initialize(user = nil, pass = nil, authz = nil,
               username: nil, password: nil, authzid: nil,
               authcid: nil, secret: nil,
               realm: nil, service: "imap", host: nil, service_name: nil,
               warn_deprecation: true, **)
  username = authcid || username || user or
    raise ArgumentError, "missing username (authcid)"
  password ||= secret || pass or raise ArgumentError, "missing password"
  authzid  ||= authz
  if warn_deprecation
    warn("WARNING: DIGEST-MD5 SASL mechanism was deprecated by RFC6331.",
         category: :deprecated)
  end

  require "digest/md5"
  require "securerandom"
  require "strscan"
  @username, @password, @authzid = username, password, authzid
  @realm        = realm
  @host         = host
  @service      = service
  @service_name = service_name
  @nc, @stage = {}, STAGE_ONE
end

Instance Attribute Details

#authcid (readonly)

Alias for #username.

[ GitHub ]

  
# File 'lib/net/imap/sasl/digest_md5_authenticator.rb', line 46

alias authcid username

#authzid (readonly)

Authorization identity: an identity to act as or on behalf of. The identity form is application protocol specific. If not provided or left blank, the server derives an authorization identity from the authentication identity. The server is responsible for verifying the client’s credentials and verifying that the identity it associates with the client’s authentication identity is allowed to act as (or on behalf of) the authorization identity.

For example, an administrator or superuser might take on another role:

imap.authenticate "DIGEST-MD5", "root", ->{passwd}, authzid: "user"
[ GitHub ]

  
# File 'lib/net/imap/sasl/digest_md5_authenticator.rb', line 64

attr_reader :authzid

#charset (readonly)

The charset sent by the server. “UTF-8” (case insensitive) is the only allowed value. nil should be interpreted as ISO 8859-1.

[ GitHub ]

  
# File 'lib/net/imap/sasl/digest_md5_authenticator.rb', line 111

attr_reader :charset

#done?Boolean (readonly)

[ GitHub ]

  
# File 'lib/net/imap/sasl/digest_md5_authenticator.rb', line 246

def done?; @stage == STAGE_DONE end

#host (readonly)

Fully qualified canonical DNS host name for the requested service.

Defaults to #realm.

[ GitHub ]

  
# File 'lib/net/imap/sasl/digest_md5_authenticator.rb', line 78

attr_reader :host

#initial_response?Boolean (readonly)

[ GitHub ]

  
# File 'lib/net/imap/sasl/digest_md5_authenticator.rb', line 194

def initial_response?; false end

#nonce (readonly)

nonce sent by the server

[ GitHub ]

  
# File 'lib/net/imap/sasl/digest_md5_authenticator.rb', line 114

attr_reader :nonce

#password (readonly)

A password or passphrase that matches the #username.

The password will be used to create the response digest.

[ GitHub ]

  
# File 'lib/net/imap/sasl/digest_md5_authenticator.rb', line 51

attr_reader :password

#qop (readonly)

qop-options sent by the server

[ GitHub ]

  
# File 'lib/net/imap/sasl/digest_md5_authenticator.rb', line 117

attr_reader :qop

#realm (readonly)

A namespace or collection of identities which contains #username.

Used by DIGEST-MD5, GSS-API, and NTLM. This is often a domain name that contains the name of the host performing the authentication.

Defaults to the last realm in the server-provided list of realms.

[ GitHub ]

  
# File 'lib/net/imap/sasl/digest_md5_authenticator.rb', line 73

attr_reader :realm

#service (readonly)

The service protocol, a registered GSSAPI service name, e.g. “imap”, “ldap”, or “xmpp”.

For ::Net::IMAP, the default is “imap” and should not be overridden. This must be set appropriately to use authenticators in other protocols.

If an IANA-registered name isn’t available, GSS-API (RFC-2743) allows the generic name “host”.

[ GitHub ]

  
# File 'lib/net/imap/sasl/digest_md5_authenticator.rb', line 90

attr_reader :service

#service_name (readonly)

The generic server name when the server is replicated.

service_name will be ignored when it is nil or identical to #host.

From RFC-2831:

The service is considered to be replicated if the client’s service-location process involves resolution using standard DNS lookup operations, and if these operations involve DNS records (such as SRV, or MX) which resolve one DNS name into a set of other DNS names. In this case, the initial name used by the client is the “serv-name”, and the final name is the “host” component.

[ GitHub ]

  
# File 'lib/net/imap/sasl/digest_md5_authenticator.rb', line 104

attr_reader :service_name

#sparams (readonly)

Parameters sent by the server are stored in this hash.

[ GitHub ]

  
# File 'lib/net/imap/sasl/digest_md5_authenticator.rb', line 107

attr_reader :sparams

#username (readonly) Also known as: #authcid

Authentication identity: the identity that matches the #password.

RFC-2831 uses the term username. “Authentication identity” is the generic term used by RFC-4422. RFC-4616 and many later RFCs abbreviate this to #authcid.

[ GitHub ]

  
# File 'lib/net/imap/sasl/digest_md5_authenticator.rb', line 45

attr_reader :username

Instance Method Details

#compute_a0(response) (private)

[ GitHub ]

  
# File 'lib/net/imap/sasl/digest_md5_authenticator.rb', line 306

def compute_a0(response)
  Digest::MD5.digest(
    [ response.values_at(:username, :realm), password ].join(":")
  )
end

#compute_a1(response) (private)

[ GitHub ]

  
# File 'lib/net/imap/sasl/digest_md5_authenticator.rb', line 312

def compute_a1(response)
  a0 = compute_a0(response)
  a1 = [ a0, response.values_at(:nonce, :cnonce) ].join(":")
  a1 << ":#{response[:authzid]}" unless response[:authzid].nil?
  a1
end

#compute_a2(response) (private)

[ GitHub ]

  
# File 'lib/net/imap/sasl/digest_md5_authenticator.rb', line 319

def compute_a2(response)
  a2 = "AUTHENTICATE:#{response[:"digest-uri"]}"
  if response[:qop] and response[:qop] =~ /^auth-(?:conf|int)$/
    a2 << ":00000000000000000000000000000000"
  end
  a2
end

#digest_uri

From RFC-2831:

Indicates the principal name of the service with which the client wishes to connect, formed from the serv-type, host, and serv-name. For example, the FTP service on “ftp.example.com” would have a “digest-uri” value of “ftp/ftp.example.com”; the SMTP server from the example above would have a “digest-uri” value of “smtp/mail3.example.com/example.com”.

[ GitHub ]

  
# File 'lib/net/imap/sasl/digest_md5_authenticator.rb', line 186

def digest_uri
  if service_name && service_name != host
    "#{service}/#{host}/#{service_name}"
  else
    "#{service}/#{host}"
  end
end

#format_response(response) (private)

[ GitHub ]

  
# File 'lib/net/imap/sasl/digest_md5_authenticator.rb', line 327

def format_response(response)
  response.map {|k, v| qdval(k.to_s, v) }.join(",")
end

#nc(nonce) (private)

[ GitHub ]

  
# File 'lib/net/imap/sasl/digest_md5_authenticator.rb', line 286

def nc(nonce)
  if @nc.has_key? nonce
    @nc[nonce] = @nc[nonce] + 1
  else
    @nc[nonce] = 1
  end
end

#parse_challenge(challenge) (private)

[ GitHub ]

  
# File 'lib/net/imap/sasl/digest_md5_authenticator.rb', line 259

def parse_challenge(challenge)
  sparams = Hash.new {|h, k| h[k] = [] }
  c = StringScanner.new(challenge)
  c.skip LIST_DELIM
  while c.scan AUTH_PARAM
    k, v = c[1], c[2]
    k = k.downcase
    if v =~ /\A"(.*)"\z/mn
      v = $1.gsub(/\\(.)/mn, '\1')
      v = split_quoted_list(v, challenge) if QUOTED_LISTABLE.include? k
    end
    sparams[k] << v
  end
  if !c.eos?
    raise DataFormatError, "Unparsable challenge: %p" % [challenge]
  elsif sparams.empty?
    raise DataFormatError, "Empty challenge: %p" % [challenge]
  end
  sparams
end

#process(challenge)

Responds to server challenge in two stages.

[ GitHub ]

  
# File 'lib/net/imap/sasl/digest_md5_authenticator.rb', line 197

def process(challenge)
  case @stage
  when STAGE_ONE
    @stage = STAGE_TWO
    @sparams = parse_challenge(challenge)
    @qop     = sparams.key?("qop") ? ["auth"] : sparams["qop"].flatten
    @nonce   = sparams["nonce"]  &.first
    @charset = sparams["charset"]&.first
    @realm ||= sparams["realm"]  &.last
    @host  ||= realm

    if !qop.include?("auth")
      raise DataFormatError, "Server does not support auth (qop = %p)" % [
        sparams["qop"]
      ]
    elsif (emptykey = REQUIRED.find { sparams[_1].empty? })
      raise DataFormatError, "Server didn't send %s (%p)" % [emptykey, challenge]
    elsif (multikey = NO_MULTIPLES.find { sparams[_1].length > 1 })
      raise DataFormatError, "Server sent multiple %s (%p)" % [multikey, challenge]
    end

    response = {
      nonce:        nonce,
      username:     username,
      realm:        realm,
      cnonce:       SecureRandom.base64(32),
      "digest-uri": digest_uri,
      qop:          "auth",
      maxbuf:       65535,
      nc:           "%08d" % nc(nonce),
      charset:      charset,
    }

    response[:authzid] = @authzid unless @authzid.nil?

    response[:response] = response_value(response)
    format_response(response)
  when STAGE_TWO
    @stage = STAGE_DONE
    raise ResponseParseError, challenge unless challenge =~ /rspauth=/
    "" # if at the second stage, return an empty string
  else
    raise ResponseParseError, challenge
  end
rescue => error
  @stage = error
  raise
end

#qdval(k, v) (private)

some responses need quoting

[ GitHub ]

  
# File 'lib/net/imap/sasl/digest_md5_authenticator.rb', line 332

def qdval(k, v)
  return if k.nil? or v.nil?
  if %w"username authzid realm nonce cnonce digest-uri qop".include? k
    v = v.gsub(/([\\"])/, "\\\1")
    return '%s="%s"' % [k, v]
  else
    return '%s=%s' % [k, v]
  end
end

#response_value(response) (private)

[ GitHub ]

  
# File 'lib/net/imap/sasl/digest_md5_authenticator.rb', line 294

def response_value(response)
  a1 = compute_a1(response)
  a2 = compute_a2(response)
  Digest::MD5.hexdigest(
    [
      Digest::MD5.hexdigest(a1),
      response.values_at(:nonce, :nc, :cnonce, :qop),
      Digest::MD5.hexdigest(a2)
    ].join(":")
  )
end

#split_quoted_list(value, challenge) (private)

[ GitHub ]

  
# File 'lib/net/imap/sasl/digest_md5_authenticator.rb', line 280

def split_quoted_list(value, challenge)
  value.split(LIST_DELIM).reject(&:empty?).tap do
    _1.any? or raise DataFormatError, "Bad Challenge: %p" % [challenge]
  end
end