class Kafka::Sasl::Scram

Constants

MECHANISMS

Public Class Methods

new(username:, password:, mechanism: 'sha256', logger:) click to toggle source
# File lib/kafka/sasl/scram.rb, line 14
def initialize(username:, password:, mechanism: 'sha256', logger:)
  @semaphore = Mutex.new
  @username = username
  @password = password
  @logger = TaggedLogger.new(logger)

  if mechanism
    @mechanism = MECHANISMS.fetch(mechanism) do
      raise Kafka::SaslScramError, "SCRAM mechanism #{mechanism} is not supported."
    end
  end
end

Public Instance Methods

authenticate!(host, encoder, decoder) click to toggle source
# File lib/kafka/sasl/scram.rb, line 35
def authenticate!(host, encoder, decoder)
  @logger.debug "Authenticating #{@username} with SASL #{@mechanism}"

  begin
    @semaphore.synchronize do
      msg = first_message
      @logger.debug "Sending first client SASL SCRAM message: #{msg}"
      encoder.write_bytes(msg)

      @server_first_message = decoder.bytes
      @logger.debug "Received first server SASL SCRAM message: #{@server_first_message}"

      msg = final_message
      @logger.debug "Sending final client SASL SCRAM message: #{msg}"
      encoder.write_bytes(msg)

      response = parse_response(decoder.bytes)
      @logger.debug "Received last server SASL SCRAM message: #{response}"

      raise FailedScramAuthentication, response['e'] if response['e']
      raise FailedScramAuthentication, "Invalid server signature" if response['v'] != server_signature
    end
  rescue EOFError => e
    raise FailedScramAuthentication, e.message
  end

  @logger.debug "SASL SCRAM authentication successful"
end
configured?() click to toggle source
# File lib/kafka/sasl/scram.rb, line 31
def configured?
  @username && @password && @mechanism
end
ident() click to toggle source
# File lib/kafka/sasl/scram.rb, line 27
def ident
  @mechanism
end

Private Instance Methods

auth_message() click to toggle source
# File lib/kafka/sasl/scram.rb, line 98
def auth_message
  [first_message_bare, @server_first_message, final_message_without_proof].join(',')
end
client_key() click to toggle source
# File lib/kafka/sasl/scram.rb, line 106
def client_key
  hmac(salted_password, 'Client Key')
end
client_proof() click to toggle source
# File lib/kafka/sasl/scram.rb, line 126
def client_proof
  Base64.strict_encode64(xor(client_key, client_signature))
end
client_signature() click to toggle source
# File lib/kafka/sasl/scram.rb, line 118
def client_signature
  hmac(stored_key, auth_message)
end
digest() click to toggle source
# File lib/kafka/sasl/scram.rb, line 164
def digest
  @digest ||= case @mechanism
              when 'SCRAM-SHA-256'
                OpenSSL::Digest::SHA256.new
              when 'SCRAM-SHA-512'
                OpenSSL::Digest::SHA512.new
              else
                raise ArgumentError, "Unknown SASL mechanism '#{@mechanism}'"
              end
end
encoded_username() click to toggle source
# File lib/kafka/sasl/scram.rb, line 156
def encoded_username
  safe_str(@username.encode(Encoding::UTF_8))
end
final_message() click to toggle source
# File lib/kafka/sasl/scram.rb, line 78
def final_message
  "#{final_message_without_proof},p=#{client_proof}"
end
final_message_without_proof() click to toggle source
# File lib/kafka/sasl/scram.rb, line 74
def final_message_without_proof
  "c=biws,r=#{rnonce}"
end
first_message() click to toggle source
# File lib/kafka/sasl/scram.rb, line 66
def first_message
  "n,,#{first_message_bare}"
end
first_message_bare() click to toggle source
# File lib/kafka/sasl/scram.rb, line 70
def first_message_bare
  "n=#{encoded_username},r=#{nonce}"
end
h(str) click to toggle source
# File lib/kafka/sasl/scram.rb, line 130
def h(str)
  digest.digest(str)
end
hi(str, salt, iterations) click to toggle source
# File lib/kafka/sasl/scram.rb, line 134
def hi(str, salt, iterations)
  OpenSSL::PKCS5.pbkdf2_hmac(
    str,
    salt,
    iterations,
    digest.size,
    digest
  )
end
hmac(data, key) click to toggle source
# File lib/kafka/sasl/scram.rb, line 144
def hmac(data, key)
  OpenSSL::HMAC.digest(digest, data, key)
end
iterations() click to toggle source
# File lib/kafka/sasl/scram.rb, line 94
def iterations
  server_data['i'].to_i
end
nonce() click to toggle source
# File lib/kafka/sasl/scram.rb, line 160
def nonce
  @nonce ||= SecureRandom.urlsafe_base64(32)
end
parse_response(data) click to toggle source
# File lib/kafka/sasl/scram.rb, line 152
def parse_response(data)
  data.split(',').map { |s| s.split('=', 2) }.to_h
end
rnonce() click to toggle source
# File lib/kafka/sasl/scram.rb, line 86
def rnonce
  server_data['r']
end
safe_str(val) click to toggle source
# File lib/kafka/sasl/scram.rb, line 175
def safe_str(val)
  val.gsub('=', '=3D').gsub(',', '=2C')
end
salt() click to toggle source
# File lib/kafka/sasl/scram.rb, line 90
def salt
  Base64.strict_decode64(server_data['s'])
end
salted_password() click to toggle source
# File lib/kafka/sasl/scram.rb, line 102
def salted_password
  hi(@password, salt, iterations)
end
server_data() click to toggle source
# File lib/kafka/sasl/scram.rb, line 82
def server_data
  parse_response(@server_first_message)
end
server_key() click to toggle source
# File lib/kafka/sasl/scram.rb, line 114
def server_key
  hmac(salted_password, 'Server Key')
end
server_signature() click to toggle source
# File lib/kafka/sasl/scram.rb, line 122
def server_signature
  Base64.strict_encode64(hmac(server_key, auth_message))
end
stored_key() click to toggle source
# File lib/kafka/sasl/scram.rb, line 110
def stored_key
  h(client_key)
end
xor(first, second) click to toggle source
# File lib/kafka/sasl/scram.rb, line 148
def xor(first, second)
  first.bytes.zip(second.bytes).map { |(a, b)| (a ^ b).chr }.join('')
end