123456789_123456789_123456789_123456789_123456789_

Class: Net::IMAP::SASL::ScramAuthenticator

Relationships & Source Files
Extension / Inclusion / Inheritance Descendants
Subclasses:
Super Chains via Extension / Inclusion / Inheritance
Instance Chain:
Inherits: Object
Defined in: lib/net/imap/sasl/scram_authenticator.rb

Overview

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

Directly supported:

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 ::Net::IMAP::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.

Constant Summary

GS2Header - Included

NO_NULL_CHARS, RFC5801_SASLNAME

Class Method Summary

Instance Attribute Summary

Instance Method Summary

ScramAlgorithm - Included

GS2Header - Included

#gs2_authzid

The RFC5801 §4 gs2-authzid header, when #authzid is not empty.

#gs2_cb_flag

The RFC5801 §4 gs2-cb-flag:

#gs2_header

The RFC5801 §4 gs2-header, which prefixes the #initial_client_response.

#gs2_saslname_encode

Encodes str to match RFC5801_SASLNAME.

Constructor Details

.new(username, password, **options) ⇒ auth_ctx .new(username:, password:, **options) ⇒ auth_ctx .new(authcid:, password:, **options) ⇒ auth_ctx

Creates an authenticator for one of the "+SCRAM-*+" ::Net::IMAP::SASL mechanisms. Each subclass defines #digest to match a specific mechanism.

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

Parameters

Any other keyword parameters are quietly ignored.

NOTE: It is the user's responsibility to enforce minimum and maximum iteration counts that are appropriate for their security context.

[ GitHub ]

  
# File 'lib/net/imap/sasl/scram_authenticator.rb', line 85

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
               max_iterations: 2**31 - 1,  # max int32
               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"

  @max_iterations = Integer max_iterations.to_int
  @min_iterations <= @max_iterations or
    raise ArgumentError, "max_iterations must be more than min_iterations"

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

  # These attrs are set from the server challenges
  @server_first_message = @snonce = @salt = @iterations = nil
  @server_error = nil

  # Memoized after @salt and @iterations have been sent.
  @salted_password = @client_key = @server_key = nil

  # These values are created and cached in response to server challenges
  @client_first_message_bare = nil
  @client_final_message_without_proof = nil
end

Instance Attribute Details

#authcid (readonly)

Alias for #username.

[ GitHub ]

  
# File 'lib/net/imap/sasl/scram_authenticator.rb', line 129

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

[ GitHub ]

  
# File 'lib/net/imap/sasl/scram_authenticator.rb', line 147

attr_reader :authzid

#cnonce (readonly)

The client nonce, generated by SecureRandom

[ GitHub ]

  
# File 'lib/net/imap/sasl/scram_authenticator.rb', line 189

attr_reader :cnonce

#done?Boolean (readonly)

Is the authentication exchange complete?

If false, another server continuation is required.

[ GitHub ]

  
# File 'lib/net/imap/sasl/scram_authenticator.rb', line 250

def done?; @state == :done end

#iterations (readonly)

The iteration count for the selected hash function and user

[ GitHub ]

  
# File 'lib/net/imap/sasl/scram_authenticator.rb', line 198

attr_reader :iterations

#max_iterations (readonly)

The maximal allowed iteration count. Higher #iterations will raise an Error.

As noted in RFC5802

A hostile server can perform a computational denial-of-service

attack on clients by sending a big iteration count value.

WARNING: The default value is 2³¹ - 1, the maximum signed 32-bit integer. This is large enough for the computation to take several minutes, and insufficient protection against hostile servers.

Note that OpenSSL::KDF.pbkdf2_hmac is implemented by a blocking C function, and cannot be interrupted by Timeout or Thread.raise. And it keeps the Global VM lock, as of v4.0 of the openssl gem, so other ruby threads will not be able to run.

To prevent a denial of service attack, this must be set to a safe value, depending on hardware and version of OpenSSL. It is the user's responsibility to enforce minimum and maximum iteration counts that are appropriate for their security context.

[ GitHub ]

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

attr_reader :max_iterations

#min_iterations (readonly)

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

WARNING: The default value (4096) is set to match guidance from both RFC5802 and RFC7677, but modern recommendations are significantly higher.

It is ultimately the server's responsibility to securely store password hashes. While this parameter can alert the user to insecure password storage and prevent insecure authentication exchange, updating the iteration count generally requires resetting the password on the server.

[ GitHub ]

  
# File 'lib/net/imap/sasl/scram_authenticator.rb', line 163

attr_reader :min_iterations

#password (readonly) Also known as: #secret

A password or passphrase that matches the #username.

[ GitHub ]

  
# File 'lib/net/imap/sasl/scram_authenticator.rb', line 132

attr_reader :password

#salt (readonly)

The salt used by the server for this user

[ GitHub ]

  
# File 'lib/net/imap/sasl/scram_authenticator.rb', line 195

attr_reader :salt

#secret (readonly)

Alias for #password.

[ GitHub ]

  
# File 'lib/net/imap/sasl/scram_authenticator.rb', line 133

alias secret password

#server_error (readonly)

An error reported by the server during the SASL exchange.

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

[ GitHub ]

  
# File 'lib/net/imap/sasl/scram_authenticator.rb', line 204

attr_reader :server_error

#server_first_message (readonly, private)

Need to store this for auth_message

[ GitHub ]

  
# File 'lib/net/imap/sasl/scram_authenticator.rb', line 262

attr_reader :server_first_message

#snonce (readonly)

The server nonce, which must start with #cnonce

[ GitHub ]

  
# File 'lib/net/imap/sasl/scram_authenticator.rb', line 192

attr_reader :snonce

#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/scram_authenticator.rb', line 128

attr_reader :username

Instance Method Details

#cbind_input (private)

[ GitHub ]

  
# File 'lib/net/imap/sasl/scram_authenticator.rb', line 325

alias cbind_input gs2_header

#client_final_message_without_proof (private)

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

[ GitHub ]

  
# File 'lib/net/imap/sasl/scram_authenticator.rb', line 314

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 (private)

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

[ GitHub ]

  
# File 'lib/net/imap/sasl/scram_authenticator.rb', line 299

def client_first_message_bare
  @client_first_message_bare ||=
    format_message(n: gs2_saslname_encode(SASL.saslprep(username)),
                   r: cnonce)
end

#client_key

[ GitHub ]

  
# File 'lib/net/imap/sasl/scram_authenticator.rb', line 210

def client_key = @client_key ||= compute_salted { super }

#compute_salted (private)

Checks for #salt and #iterations before yielding

[ GitHub ]

  
# File 'lib/net/imap/sasl/scram_authenticator.rb', line 255

def compute_salted
  salt       in String  or raise Error, "unknown salt"
  iterations in Integer or raise Error, "unknown iterations"
  yield
end

#digest

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.

[ GitHub ]

  
# File 'lib/net/imap/sasl/scram_authenticator.rb', line 220

def digest; OpenSSL::Digest.new self.class::DIGEST_NAME end

#final_message_with_proof (private)

See RFC5802 §7 client-final-message.

[ GitHub ]

  
# File 'lib/net/imap/sasl/scram_authenticator.rb', line 307

def final_message_with_proof
  proof = [client_proof].pack("m0")
  "#{client_final_message_without_proof},p=#{proof}"
end

#format_message(hash) (private)

[ GitHub ]

  
# File 'lib/net/imap/sasl/scram_authenticator.rb', line 264

def format_message(hash) hash.map { _1.join("=") }.join(",") end

#initial_client_response

See RFC5802 §7 client-first-message.

[ GitHub ]

  
# File 'lib/net/imap/sasl/scram_authenticator.rb', line 224

def initial_client_response
  "#{gs2_header}#{client_first_message_bare}"
end

#parse_challenge(challenge) (private)

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.

[ GitHub ]

  
# File 'lib/net/imap/sasl/scram_authenticator.rb', line 331

def parse_challenge(challenge)
  challenge.split(/,/).to_h {|pair| pair.split(/=/, 2) }
rescue ArgumentError
  raise Error, "unparsable challenge: %p" % [challenge]
end

#process(challenge)

responds to the server's challenges

[ GitHub ]

  
# File 'lib/net/imap/sasl/scram_authenticator.rb', line 229

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

#recv_server_final_message(server_final_message) (private)

[ GitHub ]

  
# File 'lib/net/imap/sasl/scram_authenticator.rb', line 285

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) (private)

[ GitHub ]

  
# File 'lib/net/imap/sasl/scram_authenticator.rb', line 266

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]
  max_iterations.nil? || iterations <= max_iterations or
    raise Error, "too many iterations: %d" % [iterations]
  mext = sparams["m"] and
    raise Error, "mandatory extension: %p" % [mext]
  snonce.start_with? cnonce or
    raise Error, "invalid server nonce"
end

#salted_password

[ GitHub ]

  
# File 'lib/net/imap/sasl/scram_authenticator.rb', line 207

def salted_password = @salted_password ||= compute_salted { super }

#server_key

[ GitHub ]

  
# File 'lib/net/imap/sasl/scram_authenticator.rb', line 213

def server_key = @server_key ||= compute_salted { super }