Skip to content
This repository was archived by the owner on Jan 15, 2024. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions lib/moped/errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -143,5 +143,33 @@ class ReplicaSetReconfigured < DoNotDisconnect; end

# Tag applied to unhandled exceptions on a node.
module SocketError; end

class InsufficientIterationCount < StandardError

def initialize(msg)
super(msg)
end

def self.message(required_count, given_count)
"This auth mechanism requires an iteration count of #{required_count}, but the server only requested #{given_count}"
end
end

class MissingPassword < StandardError
def initialize(msg = nil)
super(msg || 'There are no password configured')
end
end

class InvalidNonce < StandardError
attr_reader :nonce
attr_reader :rnonce

def initialize(nonce, rnonce)
@nonce = nonce
@rnonce = rnonce
super("Expected server rnonce '#{rnonce}' to start with client nonce '#{nonce}'.")
end
end
end
end
26 changes: 21 additions & 5 deletions lib/moped/node.rb
Original file line number Diff line number Diff line change
Expand Up @@ -498,12 +498,28 @@ def login(database, username, password)
getnonce = Protocol::Command.new(database, getnonce: 1)
connection.write [getnonce]
result = connection.read.documents.first
raise Errors::OperationFailure.new(getnonce, result) unless result["ok"] == 1
authenticate = Protocol::Commands::Authenticate.new(database, username, password, result["nonce"])
connection.write [authenticate]
result = connection.read.documents.first
raise Errors::OperationFailure.new(getnonce, result) unless result[Protocol::OK] == 1

if options[:auth_mech] == :scram
authenticate = Protocol::Commands::ScramAuthenticate.new(database, username, password)

connection.write [authenticate.start(result)]
result = connection.read.documents.first

connection.write [authenticate.continue(result)]
result = connection.read.documents.first

until result[Protocol::DONE]
connection.write [authenticate.finalize(result)]
result = connection.read.documents.first
end
else
authenticate = Protocol::Commands::Authenticate.new(database, username, password, result[Protocol::NONCE])
connection.write [authenticate]
result = connection.read.documents.first
end

unless result["ok"] == 1
unless result[Protocol::OK] == 1
# See if we had connectivity issues so we can retry
e = Errors::PotentialReconfiguration.new(authenticate, result)
if e.reconfiguring_replica_set?
Expand Down
3 changes: 3 additions & 0 deletions lib/moped/protocol.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ module Moped #:nodoc:
# The +Moped::Protocol+ namespace contains convenience classes for
# building all of the possible messages defined in the Mongo Wire Protocol.
module Protocol
DONE = 'done'.freeze
NONCE = 'nonce'.freeze
OK = 'ok'.freeze
end
end

Expand Down
1 change: 1 addition & 0 deletions lib/moped/protocol/commands.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ module Commands
end

require "moped/protocol/commands/authenticate"
require "moped/protocol/commands/scram_authenticate"
182 changes: 182 additions & 0 deletions lib/moped/protocol/commands/scram_authenticate.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
module Moped
module Protocol
module Commands
class ScramAuthenticate
SCRAM_SHA_1_MECHANISM = 'SCRAM-SHA-1'.freeze
CLIENT_CONTINUE_MESSAGE = { saslContinue: 1 }.freeze
CLIENT_FIRST_MESSAGE = { saslStart: 1, autoAuthorize: 1 }.freeze
CLIENT_KEY = 'Client Key'.freeze
ID = 'conversationId'.freeze
ITERATIONS = /i=(\d+)/.freeze
MIN_ITER_COUNT = 4096
PAYLOAD = 'payload'.freeze
RNONCE = /r=([^,]*)/.freeze
SALT = /s=([^,]*)/.freeze
SERVER_KEY = 'Server Key'.freeze
VERIFIER = /v=([^,]*)/.freeze

attr_reader \
:database,
:username,
:password,
:nonce,
:result

def initialize(database, username, password)
@database = database
@username = username
@password = password
end

def start(result)
@nonce = result[Protocol::NONCE]
Protocol::Command.new(database, {
saslStart: 1,
autoAuthorize: 1,
payload: client_first_message,
mechanism: SCRAM_SHA_1_MECHANISM
})
end

def continue(result)
validate_first_message!(result)
salted_password

Protocol::Command.new(database, {
saslContinue: 1,
payload: client_final_message,
conversationId: result[ID]
})
end

def finalize(result)
Protocol::Command.new(
database,
CLIENT_CONTINUE_MESSAGE.merge(
payload: client_empty_message,
conversationId: result[ID]
)
)
end

private

def client_empty_message
BSON::Binary.new(:md5, '')
end

def hmac(data, key)
OpenSSL::HMAC.digest(digest, data, key)
end

def xor(first, second)
first.bytes.zip(second.bytes).map{ |(a,b)| (a ^ b).chr }.join('')
end

def validate_first_message!(result)
validate!(result)
raise Errors::InvalidNonce.new(nonce, rnonce) unless rnonce.start_with?(nonce)
end

def client_key
@client_key ||= hmac(salted_password, CLIENT_KEY)
end

def client_proof(key, signature)
@client_proof ||= Base64.strict_encode64(xor(key, signature))
end

def client_final
@client_final ||= client_proof(client_key, client_signature(stored_key(client_key), auth_message))
end

def auth_message
@auth_message ||= "#{first_bare},#{result[PAYLOAD].data},#{without_proof}"
end

def stored_key(key)
h(key)
end

def h(string)
digest.digest(string)
end

def client_signature(key, message)
@client_signature ||= hmac(key, message)
end

def without_proof
@without_proof ||= "c=biws,r=#{rnonce}"
end

def client_final_message
BSON::Binary.new(:md5, "#{without_proof},p=#{client_final}")
end

def rnonce
@rnonce ||= payload_data.match(RNONCE)[1]
end

def validate!(result)
if result[Protocol::OK] != 1
raise Errors::AuthenticationFailure.new(
'scram.start',
{ "err" => "Invalid result ok = #{result[Protocol::OK]}" }
)
end
@result = result
end

def payload_data
result[PAYLOAD].data
end

def iterations
@iterations ||= payload_data.match(ITERATIONS)[1].to_i.tap do |i|
if i < MIN_ITER_COUNT
raise Errors::InsufficientIterationCount.new(
Errors::InsufficientIterationCount.message(MIN_ITER_COUNT, i))
end
end
end

def hi(data)
OpenSSL::PKCS5.pbkdf2_hmac_sha1(
data,
Base64.strict_decode64(salt),
iterations,
digest.size
)
end

def salt
@salt ||= payload_data.match(SALT)[1]
end

def digest
@digest ||= OpenSSL::Digest::SHA1.new.freeze
end

def salted_password
hi(hashed_password)
end

def hashed_password
unless password
raise Errors::MissingPassword
end
@hashed_password ||= Digest::MD5.hexdigest("#{username}:mongo:#{password}").encode('utf-8')
end

def client_first_message
BSON::Binary.new(:md5, "n,,#{first_bare}")
end

def first_bare
@first_bare ||= "n=#{username.encode('utf-8').gsub('=','=3D').gsub(',','=2C')},r=#{nonce}"
end
end
end
end
end