class Net::IMAP::SASL::DigestMD5Authenticator - net-imap: Ruby Standard Library Documentation

# class Net::IMAP::SASL::DigestMD5Authenticator

`Net::IMAP` authenticator for the `DIGEST-MD5` 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.

### Constants

AUTH_PARAM
DataFormatError

  Error raised when data is in the incorrect format.

LIST_DELIM
LWS
NO_MULTIPLES

  Directives which must not have multiples. The RFC states:

  > This directive may appear at most once; if multiple instances are present, the client should abort the authentication exchange.

QUOTED_LISTABLE

  Directives which are composed of one or more comma delimited tokens

QUOTED_STR
REQUIRED

  Required directives which must occur exactly once. The RFC states: >>>

      This directive is required and MUST appear exactly once; if not present,
      or if multiple instances are present, the client should abort the
      authentication exchange.

ResponseParseError

  Error raised when a response from the server is non-parsable.

STAGE_DONE
STAGE_ONE
STAGE_TWO
TOKEN

### Attributes

authcid[R]

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`.

authzid[R]

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"

charset[R]

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

host[R]

Fully qualified canonical DNS host name for the requested service.

Defaults to #realm.

nonce[R]

nonce sent by the server

password[R]

A password or passphrase that matches the `username`.

The `password` will be used to create the response digest.

qop[R]

qop-options sent by the server

realm[R]

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.

service[R]

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”.

service_name[R]

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.

sparams[R]

Parameters sent by the server are stored in this hash.

username[R]

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`.

### Public Class Methods

new(username, password, authzid = nil, **options) → authenticator click to toggle source
new(username:, password:, authzid: nil, **options) → authenticator
new(authcid:, password:, authzid: nil, **options) → authenticator

Creates an Authenticator for the “`DIGEST-MD5`” 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.

    # File net-imap-0.5.4/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

### Public Instance Methods

digest_uri() click to toggle source

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”.

    # File net-imap-0.5.4/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

done?() click to toggle source

    # File net-imap-0.5.4/lib/net/imap/sasl/digest_md5_authenticator.rb, line 246
    def done?; @stage == STAGE_DONE end

initial_response?() click to toggle source

    # File net-imap-0.5.4/lib/net/imap/sasl/digest_md5_authenticator.rb, line 194
    def initial_response?; false end

process(challenge) click to toggle source

Responds to server challenge in two stages.

    # File net-imap-0.5.4/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

### Private Instance Methods

compute_a0(response) click to toggle source

    # File net-imap-0.5.4/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) click to toggle source

    # File net-imap-0.5.4/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) click to toggle source

    # File net-imap-0.5.4/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

format_response(response) click to toggle source

    # File net-imap-0.5.4/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) click to toggle source

    # File net-imap-0.5.4/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) click to toggle source

    # File net-imap-0.5.4/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

qdval(k, v) click to toggle source

some responses need quoting

    # File net-imap-0.5.4/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) click to toggle source

    # File net-imap-0.5.4/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) click to toggle source

    # File net-imap-0.5.4/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