class Mongo::Auth::SCRAM::Conversation
Defines behaviour around a single SCRAM-SHA-1 conversation between the client and server.
@since 2.0.0
Constants
- CLIENT_CONTINUE_MESSAGE
The base client continue message.
@since 2.0.0
- CLIENT_FIRST_MESSAGE
The base client first message.
@since 2.0.0
- CLIENT_KEY
The client key string.
@since 2.0.0
- DIGEST
The digest to use for encryption.
@since 2.0.0
- DONE
The key for the done field in the responses.
@since 2.0.0
- ID
The conversation id field.
@since 2.0.0
- ITERATIONS
The iterations key in the responses.
@since 2.0.0
- PAYLOAD
The payload field.
@since 2.0.0
- RNONCE
The rnonce key in the responses.
@since 2.0.0
- SALT
The salt key in the responses.
@since 2.0.0
- SERVER_KEY
The server key string.
@since 2.0.0
- VERIFIER
The server signature verifier in the response.
@since 2.0.0
Attributes
@return [ String ] nonce The initial user nonce.
@return [ Protocol::Message ] reply The current reply in the
conversation.
@return [ User ] user The user for the conversation.
Public Class Methods
Create the new conversation.
@example Create the new conversation.
Conversation.new(user)
@param [ Auth::User ] user The user to converse about.
@since 2.0.0
# File lib/mongo/auth/scram/conversation.rb, line 216 def initialize(user) @user = user @nonce = SecureRandom.base64 @client_key = user.send(:client_key) 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.
@example Continue the conversation.
conversation.continue(reply)
@param [ Protocol::Message ] reply The reply of the previous
message.
@param [ Mongo::Server::Connection ] connection The connection being authenticated.
@return [ Protocol::Query ] The next message to send.
@since 2.0.0
# File lib/mongo/auth/scram/conversation.rb, line 112 def continue(reply, connection = nil) validate_first_message!(reply) # The salted password needs to be calculated now; otherwise, if the # client key is cached from a previous authentication, the salt in the # reply will no longer be available for when the salted password is # needed to calculate the server key. salted_password if connection && connection.features.op_msg_enabled? selector = CLIENT_CONTINUE_MESSAGE.merge(payload: client_final_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([:none], {}, selector) else Protocol::Query.new( user.auth_source, Database::COMMAND, CLIENT_CONTINUE_MESSAGE.merge(payload: client_final_message, conversationId: id), limit: -1 ) end end
Finalize the SCRAM conversation. This is meant to be iterated until the provided reply indicates the conversation is finished.
@example Finalize the conversation.
conversation.finalize(reply)
@param [ Protocol::Message ] reply The reply of the previous
message.
@param [ Mongo::Server::Connection ] connection The connection being authenticated.
@return [ Protocol::Query ] The next message to send.
@since 2.0.0
# File lib/mongo/auth/scram/conversation.rb, line 150 def finalize(reply, connection = nil) validate_final_message!(reply) 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([:none], {}, selector) else Protocol::Query.new( user.auth_source, Database::COMMAND, CLIENT_CONTINUE_MESSAGE.merge(payload: client_empty_message, conversationId: id), limit: -1 ) end end
Get the id of the conversation.
@example Get the id of the conversation.
conversation.id
@return [ Integer ] The conversation id.
@since 2.0.0
# File lib/mongo/auth/scram/conversation.rb, line 204 def id reply.documents[0][ID] end
Start the SCRAM conversation. This returns the first message that needs to be send to the server.
@example Start the conversation.
conversation.start
@param [ Mongo::Server::Connection ] connection The connection being authenticated.
@return [ Protocol::Query ] The first SCRAM conversation message.
@since 2.0.0
# File lib/mongo/auth/scram/conversation.rb, line 179 def start(connection = nil) if connection && connection.features.op_msg_enabled? selector = CLIENT_FIRST_MESSAGE.merge(payload: client_first_message, mechanism: SCRAM::MECHANISM) 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([:none], {}, selector) else Protocol::Query.new( user.auth_source, Database::COMMAND, CLIENT_FIRST_MESSAGE.merge(payload: client_first_message, mechanism: SCRAM::MECHANISM), limit: -1 ) end end
Private Instance Methods
Auth message algorithm implementation.
@api private
@see tools.ietf.org/html/rfc5802#section-3
@since 2.0.0
# File lib/mongo/auth/scram/conversation.rb, line 231 def auth_message @auth_message ||= "#{first_bare},#{reply.documents[0][PAYLOAD].data},#{without_proof}" end
Get the empty client message.
@api private
@since 2.0.0
# File lib/mongo/auth/scram/conversation.rb, line 240 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.rb, line 273 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.rb, line 251 def client_final_message BSON::Binary.new("#{without_proof},p=#{client_final}") end
Get the client first message
@api private
@see tools.ietf.org/html/rfc5802#section-3
@since 2.0.0
# File lib/mongo/auth/scram/conversation.rb, line 262 def client_first_message BSON::Binary.new("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.rb, line 284 def client_key @client_key ||= hmac(salted_password, CLIENT_KEY) user.instance_variable_set(:@client_key, @client_key) unless user.send(:client_key) @client_key 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.rb, line 297 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.rb, line 308 def client_signature(key, message) @client_signature ||= hmac(key, message) end
# File lib/mongo/auth/scram/conversation.rb, line 470 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
# File lib/mongo/auth/scram/conversation.rb, line 495 def digest @digest ||= OpenSSL::Digest::SHA1.new.freeze end
First bare implementation.
@api private
@see tools.ietf.org/html/rfc5802#section-7
@since 2.0.0
# File lib/mongo/auth/scram/conversation.rb, line 319 def first_bare @first_bare ||= "n=#{user.encoded_name},r=#{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.rb, line 330 def h(string) digest.digest(string) end
HI algorithm implementation.
@api private
@see tools.ietf.org/html/rfc5802#section-2.2
@since 2.0.0
# File lib/mongo/auth/scram/conversation.rb, line 341 def hi(data) OpenSSL::PKCS5.pbkdf2_hmac_sha1( data, Base64.strict_decode64(salt), iterations, digest.size ) 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.rb, line 357 def hmac(data, key) OpenSSL::HMAC.digest(digest, data, key) end
Get the iterations from the server response.
@api private
@since 2.0.0
# File lib/mongo/auth/scram/conversation.rb, line 366 def iterations @iterations ||= payload_data.match(ITERATIONS)[1].to_i end
Get the data from the returned payload.
@api private
@since 2.0.0
# File lib/mongo/auth/scram/conversation.rb, line 375 def payload_data reply.documents[0][PAYLOAD].data end
Get the server nonce from the payload.
@api private
@since 2.0.0
# File lib/mongo/auth/scram/conversation.rb, line 384 def rnonce @rnonce ||= payload_data.match(RNONCE)[1] end
Gets the salt from the server response.
@api private
@since 2.0.0
# File lib/mongo/auth/scram/conversation.rb, line 393 def salt @salt ||= payload_data.match(SALT)[1] end
Salted password algorithm implementation.
@api private
@see tools.ietf.org/html/rfc5802#section-3
@since 2.0.0
# File lib/mongo/auth/scram/conversation.rb, line 404 def salted_password @salted_password ||= hi(user.hashed_password) 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.rb, line 415 def server_key @server_key ||= hmac(salted_password, SERVER_KEY) 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.rb, line 426 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.rb, line 437 def stored_key(key) h(key) end
# File lib/mongo/auth/scram/conversation.rb, line 488 def validate!(reply) raise Unauthorized.new(user) unless reply.documents[0][Operation::Result::OK] == 1 @reply = reply end
# File lib/mongo/auth/scram/conversation.rb, line 476 def validate_final_message!(reply) validate!(reply) unless compare_digest(verifier, server_signature) raise Error::InvalidSignature.new(verifier, server_signature) end end
# File lib/mongo/auth/scram/conversation.rb, line 483 def validate_first_message!(reply) validate!(reply) raise Error::InvalidNonce.new(nonce, rnonce) unless rnonce.start_with?(nonce) end
Get the verifier token from the server response.
@api private
@since 2.0.0
# File lib/mongo/auth/scram/conversation.rb, line 446 def verifier @verifier ||= payload_data.match(VERIFIER)[1] 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.rb, line 457 def without_proof @without_proof ||= "c=biws,r=#{rnonce}" end
XOR operation for two strings.
@api private
@since 2.0.0
# File lib/mongo/auth/scram/conversation.rb, line 466 def xor(first, second) first.bytes.zip(second.bytes).map{ |(a,b)| (a ^ b).chr }.join('') end