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

nonce[R]

@return [ String ] nonce The initial user nonce.

reply[R]

@return [ Protocol::Message ] reply The current reply in the

conversation.
user[R]

@return [ User ] user The user for the conversation.

Public Class Methods

new(user, mechanism) click to toggle source

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(reply, connection) click to toggle source

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(reply, connection) click to toggle source

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
full_mechanism() click to toggle source
# File lib/mongo/auth/scram/conversation.rb, line 205
def full_mechanism
  MECHANISMS[@mechanism]
end
id() click to toggle source

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(connection) click to toggle source

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() click to toggle source

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
client_empty_message() click to toggle source

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() click to toggle source

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
client_final_message() click to toggle source

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
client_first_message() click to toggle source

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() click to toggle source

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(key, signature) click to toggle source

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(key, message) click to toggle source

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
compare_digest(a, b) click to toggle source
# 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
digest() click to toggle source
# 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() click to toggle source

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(string) click to toggle source

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(data) click to toggle source

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(data, key) click to toggle source

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
iterations() click to toggle source

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
payload_data() click to toggle source

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
rnonce() click to toggle source

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
salt() click to toggle source

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() click to toggle source

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() click to toggle source

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() click to toggle source

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(key) click to toggle source

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
validate!(reply, server) click to toggle source
# 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
validate_final_message!(reply, server) click to toggle source
# 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
validate_first_message!(reply, server) click to toggle source
# 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
verifier() click to toggle source

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
without_proof() click to toggle source

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(first, second) click to toggle source

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