class Mongo::Auth::SCRAM::Conversation
Defines behavior around a single SCRAM-SHA-1/256 conversation between the client and server.
@since 2.0.0 @api private
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
- 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
- MIN_ITER_COUNT
The minimum iteration count for SCRAM-SHA-256.
@api private
@since 2.6.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, mechanism)
@param [ Auth::User
] user The user to converse about. @param [ Symbol
] mechanism Authentication mechanism.
@since 2.0.0
# File lib/mongo/auth/scram/conversation.rb, line 230 def initialize(user, mechanism) unless [:scram, :scram256].include?(mechanism) raise InvalidMechanism.new(mechanism) end @user = user @nonce = SecureRandom.base64 @client_key = user.send(:client_key) @mechanism = mechanism 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 [ Server::Connection
] connection The connection being
authenticated.
@return [ Protocol::Message
] The next message to send.
@since 2.0.0
# File lib/mongo/auth/scram/conversation.rb, line 113 def continue(reply, connection) validate_first_message!(reply, connection.server) # 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([], {}, 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.
@param [ Protocol::Message
] reply The reply of the previous
message.
@param [ 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 154 def finalize(reply, connection) validate_final_message!(reply, connection.server) 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
# File lib/mongo/auth/scram/conversation.rb, line 205 def full_mechanism MECHANISMS[@mechanism] 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 217 def id reply.documents[0][ID] end
Start the SCRAM
conversation. This returns the first message that needs to be sent to the server.
@param [ 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 186 def start(connection) if connection && connection.features.op_msg_enabled? selector = CLIENT_FIRST_MESSAGE.merge( payload: client_first_message, mechanism: full_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([], {}, selector) else Protocol::Query.new( user.auth_source, Database::COMMAND, CLIENT_FIRST_MESSAGE.merge( payload: client_first_message, mechanism: full_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 250 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 259 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 292 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 270 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 281 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 303 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 316 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 327 def client_signature(key, message) @client_signature ||= hmac(key, message) end
# File lib/mongo/auth/scram/conversation.rb, line 510 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 541 def digest @digest ||= case @mechanism when :scram256 OpenSSL::Digest::SHA256.new.freeze else OpenSSL::Digest::SHA1.new.freeze end 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 338 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 349 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 360 def hi(data) case @mechanism when :scram256 OpenSSL::PKCS5.pbkdf2_hmac( data, Base64.strict_decode64(salt), iterations, digest.size, digest ) else OpenSSL::PKCS5.pbkdf2_hmac_sha1( data, Base64.strict_decode64(salt), iterations, digest.size ) end 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 387 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 396 def iterations @iterations ||= payload_data.match(ITERATIONS)[1].to_i.tap do |i| if i < MIN_ITER_COUNT raise Error::InsufficientIterationCount.new( Error::InsufficientIterationCount.message(MIN_ITER_COUNT, i)) end end end
Get the data from the returned payload.
@api private
@since 2.0.0
# File lib/mongo/auth/scram/conversation.rb, line 410 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 419 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 428 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 439 def salted_password @salted_password ||= case @mechanism when :scram256 hi(user.sasl_prepped_password) else hi(user.hashed_password) 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.rb, line 455 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 466 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 477 def stored_key(key) h(key) end
# File lib/mongo/auth/scram/conversation.rb, line 528 def validate!(reply, server) if reply.documents[0][Operation::Result::OK] != 1 raise Unauthorized.new(user, used_mechanism: full_mechanism, message: reply.documents[0]['errmsg'], server: server, ) end @reply = reply end
# File lib/mongo/auth/scram/conversation.rb, line 516 def validate_final_message!(reply, server) validate!(reply, server) unless compare_digest(verifier, server_signature) raise Error::InvalidSignature.new(verifier, server_signature) end end
# File lib/mongo/auth/scram/conversation.rb, line 523 def validate_first_message!(reply, server) validate!(reply, server) 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 486 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 497 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 506 def xor(first, second) first.bytes.zip(second.bytes).map{ |(a,b)| (a ^ b).chr }.join('') end