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

# class Net::IMAP::SASL::ScramAuthenticator

Abstract base class for the “`SCRAM-*`” family of `SASL` mechanisms, defined in RFC5802. Use via `Net::IMAP#authenticate`.

Directly supported:

- `SCRAM-SHA-1` — `ScramSHA1Authenticator`

- `SCRAM-SHA-256` — `ScramSHA256Authenticator`

New `SCRAM-*` mechanisms can easily be added for any hash algorithm supported by OpenSSL::Digest. Subclasses need only set an appropriate `DIGEST_NAME` constant.

### SCRAM algorithm¶ ↑

See the documentation and method definitions on `ScramAlgorithm` for an overview of the algorithm. The different mechanisms differ only by which hash function that is used (or by support for channel binding with `-PLUS`).

See also the methods on `GS2Header`.

#### Server messages¶ ↑

As server messages are received, they are validated and loaded into the various attributes, e.g: `snonce`, `salt`, `iterations`, verifier, `server_error`, etc.

Unlike many other `SASL` mechanisms, the `SCRAM-*` family supports mutual authentication and can return server error data in the server messages. If `process` raises an `Error` for the server-final-message, then `server_error` may contain error details.

### TLS Channel binding¶ ↑

The `SCRAM-*-PLUS` mechanisms and channel binding are not supported yet.

### Caching SCRAM secrets¶ ↑

Caching of salted_password, client_key, stored_key, and server_key is not supported yet.

### 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. For example, an administrator or superuser might take on another role:

    imap.authenticate "SCRAM-SHA-256", "root", passwd, authzid: "user"

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.

cnonce[R]

The client nonce, generated by SecureRandom

iterations[R]

The iteration count for the selected hash function and user

min_iterations[R]

The minimal allowed iteration count. Lower `iterations` will raise an `Error`.

password[R]

A password or passphrase that matches the `username`.

salt[R]

The salt used by the server for this user

secret[R]

A password or passphrase that matches the `username`.

server_error[R]

An error reported by the server during the SASL exchange.

Does not include errors reported by the protocol, e.g. `Net::IMAP::NoResponseError`.

server_first_message[R]

Need to store this for auth_message

snonce[R]

The server nonce, which must start with `cnonce`

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, **options) → auth_ctx click to toggle source
new(username:, password:, **options) → auth_ctx
new(authcid:, password:, **options) → auth_ctx

Creates an authenticator for one of the “`SCRAM-*`” `SASL` mechanisms. Each subclass defines `digest` to match a specific mechanism.

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

### Parameters¶ ↑

- `authcid` ― Identity whose `password` is used.

  `username` - An alias for `authcid`.

- `password` ― Password or passphrase associated with this `username`.

- optional `authzid` ― Alternate identity to act as or on behalf of.

- optional `min_iterations` - Overrides the default value (4096).

Any other keyword parameters are quietly ignored.

    # File net-imap-0.5.4/lib/net/imap/sasl/scram_authenticator.rb, line 80
    def initialize(username_arg = nil, password_arg = nil,
                   authcid: nil, username: nil,
                   authzid: nil,
                   password: nil, secret: nil,
                   min_iterations: 4096, # see both RFC5802 and RFC7677
                   cnonce: nil, # must only be set in tests
                   **options)
      @username = username || username_arg || authcid or
        raise ArgumentError, "missing username (authcid)"
      @password = password || secret || password_arg or
        raise ArgumentError, "missing password"
      @authzid = authzid

      @min_iterations = Integer min_iterations
      @min_iterations.positive? or
        raise ArgumentError, "min_iterations must be positive"

      @cnonce = cnonce || SecureRandom.base64(32)
    end

### Public Instance Methods

digest() click to toggle source

Returns a new OpenSSL::Digest object, set to the appropriate hash function for the chosen mechanism.

The class’s `DIGEST_NAME` constant must be set to the name of an algorithm supported by OpenSSL::Digest.

    # File net-imap-0.5.4/lib/net/imap/sasl/scram_authenticator.rb, line 155
    def digest; OpenSSL::Digest.new self.class::DIGEST_NAME end

done?() click to toggle source

Is the authentication exchange complete?

If false, another server continuation is required.

    # File net-imap-0.5.4/lib/net/imap/sasl/scram_authenticator.rb, line 185
    def done?; @state == :done end

initial_client_response() click to toggle source

See RFC5802 §7 `client-first-message`.

    # File net-imap-0.5.4/lib/net/imap/sasl/scram_authenticator.rb, line 159
    def initial_client_response
      "#{gs2_header}#{client_first_message_bare}"
    end

process(challenge) click to toggle source

responds to the server’s challenges

    # File net-imap-0.5.4/lib/net/imap/sasl/scram_authenticator.rb, line 164
    def process(challenge)
      case (@state ||= :initial_client_response)
      when :initial_client_response
        initial_client_response.tap { @state = :server_first_message }
      when :server_first_message
        recv_server_first_message challenge
        final_message_with_proof.tap { @state = :server_final_message }
      when :server_final_message
        recv_server_final_message challenge
        "".tap { @state = :done }
      else
        raise Error, "server sent after complete, %p" % [challenge]
      end
    rescue Exception => ex
      @state = ex
      raise
    end

### Private Instance Methods

client_final_message_without_proof() click to toggle source

See RFC5802 §7 `client-final-message-without-proof`.

    # File net-imap-0.5.4/lib/net/imap/sasl/scram_authenticator.rb, line 240
    def client_final_message_without_proof
      @client_final_message_without_proof ||=
        format_message(c: [cbind_input].pack("m0"), # channel-binding
                       r: snonce)                   # nonce
    end

client_first_message_bare() click to toggle source

See RFC5802 §7 `client-first-message-bare`.

    # File net-imap-0.5.4/lib/net/imap/sasl/scram_authenticator.rb, line 225
    def client_first_message_bare
      @client_first_message_bare ||=
        format_message(n: gs2_saslname_encode(SASL.saslprep(username)),
                       r: cnonce)
    end

final_message_with_proof() click to toggle source

See RFC5802 §7 `client-final-message`.

    # File net-imap-0.5.4/lib/net/imap/sasl/scram_authenticator.rb, line 233
    def final_message_with_proof
      proof = [client_proof].pack("m0")
      "#{client_final_message_without_proof},p=#{proof}"
    end

format_message(hash) click to toggle source

    # File net-imap-0.5.4/lib/net/imap/sasl/scram_authenticator.rb, line 192
    def format_message(hash) hash.map { _1.join("=") }.join(",") end

parse_challenge(challenge) click to toggle source

RFC5802 specifies “that the order of attributes in client or server messages is fixed, with the exception of extension attributes”, but this parses it simply as a hash, without respect to order. Note that repeated keys (violating the spec) will use the last value.

    # File net-imap-0.5.4/lib/net/imap/sasl/scram_authenticator.rb, line 257
    def parse_challenge(challenge)
      challenge.split(/,/).to_h {|pair| pair.split(/=/, 2) }
    rescue ArgumentError
      raise Error, "unparsable challenge: %p" % [challenge]
    end

recv_server_final_message(server_final_message) click to toggle source

    # File net-imap-0.5.4/lib/net/imap/sasl/scram_authenticator.rb, line 211
    def recv_server_final_message(server_final_message)
      sparams = parse_challenge server_final_message
      @server_error = sparams["e"] and
        raise Error, "server error: %s" % [server_error]
      verifier = sparams["v"].unpack1("m") or
        raise Error, "server did not send verifier"
      verifier == server_signature or
        raise Error, "server verify failed: %p != %p" % [
          server_signature, verifier
        ]
    end

recv_server_first_message(server_first_message) click to toggle source

    # File net-imap-0.5.4/lib/net/imap/sasl/scram_authenticator.rb, line 194
    def recv_server_first_message(server_first_message)
      @server_first_message = server_first_message
      sparams = parse_challenge server_first_message
      @snonce = sparams["r"] or
        raise Error, "server did not send nonce"
      @salt = sparams["s"]&.unpack1("m") or
        raise Error, "server did not send salt"
      @iterations = sparams["i"]&.then {|i| Integer i } or
        raise Error, "server did not send iteration count"
      min_iterations <= iterations or
        raise Error, "too few iterations: %d" % [iterations]
      mext = sparams["m"] and
        raise Error, "mandatory extension: %p" % [mext]
      snonce.start_with? cnonce or
        raise Error, "invalid server nonce"
    end