class Mongo::Auth::SCRAM::Conversation

Defines behavior around a single SCRAM-SHA-1/256 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

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 222
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 = nil) 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 [ 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 111
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([], {}, 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 = nil) click to toggle source

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 149
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([], {}, 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 197
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 209
def id
  reply.documents[0][ID]
end
start(connection = nil) click to toggle source

Start the SCRAM conversation. This returns the first message that needs to be sent 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 178
def start(connection = nil)
  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 242
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 251
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 284
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 262
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 273
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 295
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 308
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 319
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 502
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 529
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 330
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 341
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 352
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 379
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 388
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 402
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 411
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 420
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 431
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 447
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 458
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 469
def stored_key(key)
  h(key)
end
validate!(reply) click to toggle source
# File lib/mongo/auth/scram/conversation.rb, line 520
def validate!(reply)
  if reply.documents[0][Operation::Result::OK] != 1
    raise Unauthorized.new(user, full_mechanism)
  end
  @reply = reply
end
validate_final_message!(reply) click to toggle source
# File lib/mongo/auth/scram/conversation.rb, line 508
def validate_final_message!(reply)
  validate!(reply)
  unless compare_digest(verifier, server_signature)
    raise Error::InvalidSignature.new(verifier, server_signature)
  end
end
validate_first_message!(reply) click to toggle source
# File lib/mongo/auth/scram/conversation.rb, line 515
def validate_first_message!(reply)
  validate!(reply)
  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 478
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 489
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 498
def xor(first, second)
  first.bytes.zip(second.bytes).map{ |(a,b)| (a ^ b).chr }.join('')
end