class Mongo::Auth::ScramConversationBase
Defines common behavior around authentication conversations between the client and the server.
@api private
Constants
- MIN_ITER_COUNT
The minimum iteration count for SCRAM-SHA-1 and SCRAM-SHA-256.
Attributes
Auth
message algorithm implementation.
@api private
@see tools.ietf.org/html/rfc5802#section-3
@since 2.0.0
@return [ String ] client_nonce
The client nonce.
Get the id of the conversation.
@example Get the id of the conversation.
conversation.id
@return [ Integer ] The conversation id.
Get the iterations from the server response.
@api private
@since 2.0.0
Get the data from the returned payload.
@api private
@since 2.0.0
Gets the salt from the server response.
@api private
@since 2.0.0
Get the server nonce from the payload.
@api private
@since 2.0.0
Public Class Methods
Create the new conversation.
@param [ Auth::User
] user The user to converse about. @param [ String | nil ] client_nonce
The client nonce to use.
If this conversation is created for a connection that performed speculative authentication, this client nonce must be equal to the client nonce used for speculative authentication; otherwise, the client nonce must not be specified.
Mongo::Auth::ConversationBase::new
# File lib/mongo/auth/scram_conversation_base.rb, line 35 def initialize(user, connection, client_nonce: nil) super @client_nonce = client_nonce || SecureRandom.base64 end
Public Instance Methods
Continue the SCRAM conversation. This sends the client final message to the server after setting the reply from the previous server communication.
@param [ BSON::Document ] reply_document The reply document of the
previous message.
@param [ Server::Connection
] connection The connection being
authenticated.
@return [ Protocol::Message
] The next message to send.
# File lib/mongo/auth/scram_conversation_base.rb, line 70 def continue(reply_document, connection) @id = reply_document['conversationId'] payload_data = reply_document['payload'].data parsed_data = parse_payload(payload_data) @server_nonce = parsed_data.fetch('r') @salt = Base64.strict_decode64(parsed_data.fetch('s')) @iterations = parsed_data.fetch('i').to_i.tap do |i| if i < MIN_ITER_COUNT raise Error::InsufficientIterationCount.new( Error::InsufficientIterationCount.message(MIN_ITER_COUNT, i)) end end @auth_message = "#{first_bare},#{payload_data},#{without_proof}" validate_server_nonce! selector = CLIENT_CONTINUE_MESSAGE.merge( payload: client_final_message, conversationId: id, ) if connection && connection.features.op_msg_enabled? selector[Protocol::Msg::DATABASE_IDENTIFIER] = user.auth_source cluster_time = connection.mongos? && connection.cluster_time selector[Operation::CLUSTER_TIME] = cluster_time if cluster_time Protocol::Msg.new([], {}, selector) else Protocol::Query.new( user.auth_source, Database::COMMAND, selector, limit: -1, ) end end
Finalize the SCRAM conversation. This is meant to be iterated until the provided reply indicates the conversation is finished.
@param [ Server::Connection
] connection The connection being authenticated.
@return [ Protocol::Query
] The next message to send.
# File lib/mongo/auth/scram_conversation_base.rb, line 120 def finalize(connection) if connection && connection.features.op_msg_enabled? selector = CLIENT_CONTINUE_MESSAGE.merge( payload: client_empty_message, conversationId: id, ) selector[Protocol::Msg::DATABASE_IDENTIFIER] = user.auth_source cluster_time = connection.mongos? && connection.cluster_time selector[Operation::CLUSTER_TIME] = cluster_time if cluster_time Protocol::Msg.new([], {}, selector) else Protocol::Query.new( user.auth_source, Database::COMMAND, CLIENT_CONTINUE_MESSAGE.merge( payload: client_empty_message, conversationId: id, ), limit: -1, ) end end
Processes the second response from the server.
@param [ BSON::Document ] reply_document The reply document of the
continue response.
# File lib/mongo/auth/scram_conversation_base.rb, line 109 def process_continue_response(reply_document) payload_data = parse_payload(reply_document['payload'].data) check_server_signature(payload_data) end
Whether the client verified the ServerSignature from the server.
@see jira.mongodb.org/browse/SECURITY-621
@return [ true | fase ] Whether the server's signature was verified.
# File lib/mongo/auth/scram_conversation_base.rb, line 56 def server_verified? !!@server_verified end
Returns the hash to provide to the server in the handshake as value of the speculativeAuthenticate key.
If the auth mechanism does not support speculative authentication, this method returns nil.
@return [ Hash | nil ] Speculative authentication document.
# File lib/mongo/auth/scram_conversation_base.rb, line 150 def speculative_auth_document client_first_document.merge(db: user.auth_source) end
Private Instance Methods
@api private
# File lib/mongo/auth/scram_conversation_base.rb, line 336 def cache_key(*extra) [user.password, salt, iterations, @mechanism] + extra end
Looks for field 'v' in payload data, if it is present verifies the server signature. If verification succeeds, sets @server_verified to true. If verification fails, raises InvalidSignature.
This method can be called from different conversation steps depending on whether the short SCRAM conversation is used.
# File lib/mongo/auth/scram_conversation_base.rb, line 229 def check_server_signature(payload_data) if verifier = payload_data['v'] if compare_digest(verifier, server_signature) @server_verified = true else raise Error::InvalidSignature.new(verifier, server_signature) end end end
Get the empty client message.
@api private
@since 2.0.0
# File lib/mongo/auth/scram_conversation_base.rb, line 195 def client_empty_message BSON::Binary.new('') end
Client
final implementation.
@api private
@see tools.ietf.org/html/rfc5802#section-7
@since 2.0.0
# File lib/mongo/auth/scram_conversation_base.rb, line 217 def client_final @client_final ||= client_proof(client_key, client_signature(stored_key(client_key), auth_message)) end
Get the final client message.
@api private
@see tools.ietf.org/html/rfc5802#section-3
@since 2.0.0
# File lib/mongo/auth/scram_conversation_base.rb, line 206 def client_final_message BSON::Binary.new("#{without_proof},p=#{client_final}") end
# File lib/mongo/auth/scram_conversation_base.rb, line 172 def client_first_message_options {skipEmptyExchange: true} end
@see tools.ietf.org/html/rfc5802#section-3
# File lib/mongo/auth/scram_conversation_base.rb, line 177 def client_first_payload "n,,#{first_bare}" end
Client
key algorithm implementation.
@api private
@see tools.ietf.org/html/rfc5802#section-3
@since 2.0.0
# File lib/mongo/auth/scram_conversation_base.rb, line 246 def client_key @client_key ||= CredentialCache.cache(cache_key(:client_key)) do hmac(salted_password, 'Client Key') end end
Client
proof algorithm implementation.
@api private
@see tools.ietf.org/html/rfc5802#section-3
@since 2.0.0
# File lib/mongo/auth/scram_conversation_base.rb, line 259 def client_proof(key, signature) @client_proof ||= Base64.strict_encode64(xor(key, signature)) end
Client
signature algorithm implementation.
@api private
@see tools.ietf.org/html/rfc5802#section-3
@since 2.0.0
# File lib/mongo/auth/scram_conversation_base.rb, line 270 def client_signature(key, message) @client_signature ||= hmac(key, message) end
# File lib/mongo/auth/scram_conversation_base.rb, line 395 def compare_digest(a, b) check = a.bytesize ^ b.bytesize a.bytes.zip(b.bytes){ |x, y| check |= x ^ y.to_i } check == 0 end
First bare implementation.
@api private
@see tools.ietf.org/html/rfc5802#section-7
@since 2.0.0
# File lib/mongo/auth/scram_conversation_base.rb, line 281 def first_bare @first_bare ||= "n=#{user.encoded_name},r=#{client_nonce}" end
H algorithm implementation.
@api private
@see tools.ietf.org/html/rfc5802#section-2.2
@since 2.0.0
# File lib/mongo/auth/scram_conversation_base.rb, line 292 def h(string) digest.digest(string) end
HMAC algorithm implementation.
@api private
@see tools.ietf.org/html/rfc5802#section-2.2
@since 2.0.0
# File lib/mongo/auth/scram_conversation_base.rb, line 303 def hmac(data, key) OpenSSL::HMAC.digest(digest, data, key) end
Parses a payload like a=value,b=value2 into a hash like {'a' => 'value', 'b' => 'value2'}.
@param [ String ] payload The payload to parse.
@return [ Hash ] Parsed key-value pairs.
# File lib/mongo/auth/scram_conversation_base.rb, line 162 def parse_payload(payload) Hash[payload.split(',').reject { |v| v == '' }.map do |pair| k, v, = pair.split('=', 2) if k == '' raise Error::InvalidServerAuthResponse, 'Payload malformed: missing key' end [k, v] end] end
Server
key algorithm implementation.
@api private
@see tools.ietf.org/html/rfc5802#section-3
@since 2.0.0
# File lib/mongo/auth/scram_conversation_base.rb, line 347 def server_key @server_key ||= CredentialCache.cache(cache_key(:server_key)) do hmac(salted_password, 'Server Key') end end
Server
signature algorithm implementation.
@api private
@see tools.ietf.org/html/rfc5802#section-3
@since 2.0.0
# File lib/mongo/auth/scram_conversation_base.rb, line 360 def server_signature @server_signature ||= Base64.strict_encode64(hmac(server_key, auth_message)) end
Stored key algorithm implementation.
@api private
@see tools.ietf.org/html/rfc5802#section-3
@since 2.0.0
# File lib/mongo/auth/scram_conversation_base.rb, line 371 def stored_key(key) h(key) end
Get the without proof message.
@api private
@see tools.ietf.org/html/rfc5802#section-7
@since 2.0.0
# File lib/mongo/auth/scram_conversation_base.rb, line 382 def without_proof @without_proof ||= "c=biws,r=#{server_nonce}" end
XOR operation for two strings.
@api private
@since 2.0.0
# File lib/mongo/auth/scram_conversation_base.rb, line 391 def xor(first, second) first.bytes.zip(second.bytes).map{ |(a,b)| (a ^ b).chr }.join('') end