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:)
  @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 34
def authenticate!(host, encoder, decoder)
  @logger.debug "Authenticating #{@username} with SASL #{@mechanism}"

  begin
    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
  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 30
def configured?
  @username && @password && @mechanism
end
ident() click to toggle source
# File lib/kafka/sasl/scram.rb, line 26
def ident
  @mechanism
end

Private Instance Methods

auth_message() click to toggle source
# File lib/kafka/sasl/scram.rb, line 95
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 103
def client_key
  hmac(salted_password, 'Client Key')
end
client_proof() click to toggle source
# File lib/kafka/sasl/scram.rb, line 123
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 115
def client_signature
  hmac(stored_key, auth_message)
end
digest() click to toggle source
# File lib/kafka/sasl/scram.rb, line 161
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 153
def encoded_username
  safe_str(@username.encode(Encoding::UTF_8))
end
final_message() click to toggle source
# File lib/kafka/sasl/scram.rb, line 75
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 71
def final_message_without_proof
  "c=biws,r=#{rnonce}"
end
first_message() click to toggle source
# File lib/kafka/sasl/scram.rb, line 63
def first_message
  "n,,#{first_message_bare}"
end
first_message_bare() click to toggle source
# File lib/kafka/sasl/scram.rb, line 67
def first_message_bare
  "n=#{encoded_username},r=#{nonce}"
end
h(str) click to toggle source
# File lib/kafka/sasl/scram.rb, line 127
def h(str)
  digest.digest(str)
end
hi(str, salt, iterations) click to toggle source
# File lib/kafka/sasl/scram.rb, line 131
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 141
def hmac(data, key)
  OpenSSL::HMAC.digest(digest, data, key)
end
iterations() click to toggle source
# File lib/kafka/sasl/scram.rb, line 91
def iterations
  server_data['i'].to_i
end
nonce() click to toggle source
# File lib/kafka/sasl/scram.rb, line 157
def nonce
  @nonce ||= SecureRandom.urlsafe_base64(32)
end
parse_response(data) click to toggle source
# File lib/kafka/sasl/scram.rb, line 149
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 83
def rnonce
  server_data['r']
end
safe_str(val) click to toggle source
# File lib/kafka/sasl/scram.rb, line 172
def safe_str(val)
  val.gsub('=', '=3D').gsub(',', '=2C')
end
salt() click to toggle source
# File lib/kafka/sasl/scram.rb, line 87
def salt
  Base64.strict_decode64(server_data['s'])
end
salted_password() click to toggle source
# File lib/kafka/sasl/scram.rb, line 99
def salted_password
  hi(@password, salt, iterations)
end
server_data() click to toggle source
# File lib/kafka/sasl/scram.rb, line 79
def server_data
  parse_response(@server_first_message)
end
server_key() click to toggle source
# File lib/kafka/sasl/scram.rb, line 111
def server_key
  hmac(salted_password, 'Server Key')
end
server_signature() click to toggle source
# File lib/kafka/sasl/scram.rb, line 119
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 107
def stored_key
  h(client_key)
end
xor(first, second) click to toggle source
# File lib/kafka/sasl/scram.rb, line 145
def xor(first, second)
  first.bytes.zip(second.bytes).map { |(a, b)| (a ^ b).chr }.join('')
end