Class: Net::IMAP::SASL::ScramAuthenticator
| Relationships & Source Files | |
| Extension / Inclusion / Inheritance Descendants | |
|
Subclasses:
|
|
| Super Chains via Extension / Inclusion / Inheritance | |
|
Instance Chain:
self,
ScramAlgorithm,
GS2Header
|
|
| 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:
SCRAM-SHA-1---ScramSHA1AuthenticatorSCRAM-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 ::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
Class Method Summary
-
.new(username, password, **options) ⇒ auth_ctx
constructor
Creates an authenticator for one of the "+SCRAM-*+"
::Net::IMAP::SASLmechanisms.
Instance Attribute Summary
-
#authcid
readonly
Alias for #username.
-
#authzid
readonly
Authorization identity: an identity to act as or on behalf of.
-
#cnonce
readonly
The client nonce, generated by SecureRandom.
-
#done? ⇒ Boolean
readonly
Is the authentication exchange complete?
-
#iterations
readonly
The iteration count for the selected hash function and user.
-
#max_iterations
readonly
The maximal allowed iteration count.
-
#min_iterations
readonly
The minimal allowed iteration count.
-
#password
(also: #secret)
readonly
A password or passphrase that matches the #username.
-
#salt
readonly
The salt used by the server for this user.
-
#secret
readonly
Alias for #password.
-
#server_error
readonly
An error reported by the server during the SASL exchange.
-
#snonce
readonly
The server nonce, which must start with #cnonce
-
#username
(also: #authcid)
readonly
Authentication identity: the identity that matches the #password.
-
#server_first_message
readonly
private
Need to store this for auth_message.
Instance Method Summary
-
#client_key
Memoized ScramAlgorithm#client_key (needs #salt and #iterations).
-
#digest
Returns a new
OpenSSL::Digestobject, set to the appropriate hash function for the chosen mechanism. -
#initial_client_response
See RFC5802 §7
client-first-message. -
#process(challenge)
responds to the server's challenges.
-
#salted_password
Memoized ScramAlgorithm#salted_password (needs #salt and #iterations).
-
#server_key
Memoized ScramAlgorithm#server_key (needs #salt and #iterations).
-
#cbind_input
private
Alias for GS2Header#gs2_header.
-
#client_final_message_without_proof
private
See RFC5802 §7
client-final-message-without-proof. -
#client_first_message_bare
private
See RFC5802 §7
client-first-message-bare. -
#compute_salted
private
Checks for #salt and #iterations before yielding.
-
#final_message_with_proof
private
See RFC5802 §7
client-final-message. - #format_message(hash) private
-
#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.
- #recv_server_final_message(server_final_message) private
- #recv_server_first_message(server_first_message) private
ScramAlgorithm - Included
| #auth_message, #client_key, #client_proof, #client_signature, #H, #Hi, #HMAC, #Normalize, #salted_password, #server_key, #server_signature, #stored_key, #XOR |
GS2Header - Included
| #gs2_authzid | The RFC5801 §4 |
| #gs2_cb_flag | The RFC5801 §4 |
| #gs2_header | The RFC5801 §4 |
| #gs2_saslname_encode | Encodes |
Constructor Details
.new(username, password, **options) ⇒ auth_ctx
.new(username:, password:, **options) ⇒ auth_ctx
.new(authcid:, password:, **options) ⇒ auth_ctx
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
-
#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).
-
optional #max_iterations - Overrides the default value (2³¹ - 1).
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.
# 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 **) @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.
# 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.
# File 'lib/net/imap/sasl/scram_authenticator.rb', line 147
attr_reader :authzid
#cnonce (readonly)
The client nonce, generated by SecureRandom
# 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.
# 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
# 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.
# 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.
# 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.
# File 'lib/net/imap/sasl/scram_authenticator.rb', line 132
attr_reader :password
#salt (readonly)
The salt used by the server for this user
# File 'lib/net/imap/sasl/scram_authenticator.rb', line 195
attr_reader :salt
#secret (readonly)
Alias for #password.
# 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.
# 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
# File 'lib/net/imap/sasl/scram_authenticator.rb', line 262
attr_reader :
#snonce (readonly)
The server nonce, which must start with #cnonce
# File 'lib/net/imap/sasl/scram_authenticator.rb', line 192
attr_reader :snonce
#username (readonly) Also known as: #authcid
# File 'lib/net/imap/sasl/scram_authenticator.rb', line 128
attr_reader :username
Instance Method Details
#cbind_input (private)
Alias for GS2Header#gs2_header.
# 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.
# File 'lib/net/imap/sasl/scram_authenticator.rb', line 314
def @client_final_message_without_proof ||= (c: [cbind_input].pack("m0"), # channel-binding r: snonce) # nonce end
#client_first_message_bare (private)
See RFC5802 §7
client-first-message-bare.
#client_key
Memoized ScramAlgorithm#client_key (needs #salt and #iterations)
# 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
# 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.
# 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.
# File 'lib/net/imap/sasl/scram_authenticator.rb', line 307
def proof = [client_proof].pack("m0") "#{},p=#{proof}" end
#format_message(hash) (private)
[ GitHub ]# File 'lib/net/imap/sasl/scram_authenticator.rb', line 264
def (hash) hash.map { _1.join("=") }.join(",") end
#initial_client_response
See RFC5802 §7
client-first-message.
# File 'lib/net/imap/sasl/scram_authenticator.rb', line 224
def initial_client_response "#{gs2_header}#{}" 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.
# 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
# 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 = : } when : challenge .tap { @state = : } when : 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 () sparams = parse_challenge @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 () @server_first_message = sparams = parse_challenge @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
Memoized ScramAlgorithm#salted_password (needs #salt and #iterations)
# File 'lib/net/imap/sasl/scram_authenticator.rb', line 207
def salted_password = @salted_password ||= compute_salted { super }
#server_key
Memoized ScramAlgorithm#server_key (needs #salt and #iterations)
# File 'lib/net/imap/sasl/scram_authenticator.rb', line 213
def server_key = @server_key ||= compute_salted { super }