# This file is part of the ruby-dbus project # Copyright (C) 2007 Arnaud Cornet and Paul van Tilburg # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License, version 2.1 as published by the Free Software Foundation. # See the file "COPYING" for the exact licensing terms. require "rbconfig" module DBus # Exception raised when authentication fails somehow. class AuthenticationFailed < Exception end # = General class for authentication. class Authenticator # Returns the name of the authenticator. def name self.class.to_s.upcase.sub(/.*::/, "") end end # = Anonymous authentication class class Anonymous < Authenticator def authenticate "527562792044427573" # Hex encoded version of "Ruby DBus" end end # = External authentication class # # Class for 'external' type authentication. class External < Authenticator # Performs the authentication. def authenticate # Take the user id (eg integer 1000) make a string out of it "1000", take # each character and determin hex value "1" => 0x31, "0" => 0x30. You # obtain for "1000" => 31303030 This is what the server is expecting. # Why? I dunno. How did I come to that conclusion? by looking at rbus # code. I have no idea how he found that out. Process.uid.to_s.split(//).map { |d| d.ord.to_s(16) }.join end end # = Authentication class using SHA1 crypto algorithm # # Class for 'CookieSHA1' type authentication. # Implements the AUTH DBUS_COOKIE_SHA1 mechanism. class DBusCookieSHA1 < Authenticator # the autenticate method (called in stage one of authentification) def authenticate require "etc" # number of retries we have for auth @retries = 1 hex_encode(Etc.getlogin).to_s # server expects it to be binary end # returns the modules name def name "DBUS_COOKIE_SHA1" end # handles the interesting crypto stuff, check the rbus-project for more info: http://rbus.rubyforge.org/ def data(hexdata) require "digest/sha1" data = hex_decode(hexdata) # name of cookie file, id of cookie in file, servers random challenge context, id, s_challenge = data.split(" ") # Random client challenge c_challenge = 1.upto(s_challenge.bytesize / 2).map { rand(255).to_s }.join # Search cookie file for id path = File.join(ENV["HOME"], ".dbus-keyrings", context) DBus.logger.debug "path: #{path.inspect}" File.foreach(path) do |line| if line.index(id) == 0 # Right line of file, read cookie cookie = line.split(" ")[2].chomp DBus.logger.debug "cookie: #{cookie.inspect}" # Concatenate and encrypt to_encrypt = [s_challenge, c_challenge, cookie].join(":") sha = Digest::SHA1.hexdigest(to_encrypt) # the almighty tcp server wants everything hex encoded hex_response = hex_encode("#{c_challenge} #{sha}") # Return response response = [:AuthOk, hex_response] return response end end # a little rescue magic unless @retries <= 0 puts "ERROR: Could not auth, will now exit." puts "ERROR: Unable to locate cookie, retry in 1 second." @retries -= 1 sleep 1 data(hexdata) end end # encode plain to hex def hex_encode(plain) return nil if plain.nil? plain.to_s.unpack("H*")[0] end # decode hex to plain def hex_decode(encoded) encoded.scan(/[[:xdigit:]]{2}/).map { |h| h.hex.chr }.join end end # DBusCookieSHA1 class ends here # Note: this following stuff is tested with External authenticator only! # = Authentication client class. # # Class tha performs the actional authentication. class Client # Create a new authentication client. def initialize(socket) @socket = socket @state = nil @auth_list = [External, DBusCookieSHA1, Anonymous] end # Start the authentication process. def authenticate if RbConfig::CONFIG["target_os"] =~ /freebsd/ @socket.sendmsg(0.chr, 0, nil, [:SOCKET, :SCM_CREDS, ""]) else @socket.write(0.chr) end next_authenticator @state = :Starting while @state != :Authenticated r = next_state return r if !r end true end ########## private ########## # Send an authentication method _meth_ with arguments _args_ to the # server. def send(meth, *args) o = ([meth] + args).join(" ") @socket.write(o + "\r\n") end # Try authentication using the next authenticator. def next_authenticator raise AuthenticationFailed if @auth_list.empty? @authenticator = @auth_list.shift.new auth_msg = ["AUTH", @authenticator.name, @authenticator.authenticate] DBus.logger.debug "auth_msg: #{auth_msg.inspect}" send(auth_msg) rescue AuthenticationFailed @socket.close raise end # Read data (a buffer) from the bus until CR LF is encountered. # Return the buffer without the CR LF characters. def next_msg data = "" crlf = "\r\n" left = 1024 # 1024 byte, no idea if it's ever getting bigger while left > 0 buf = @socket.read(left > 1 ? 1 : left) break if buf.nil? left -= buf.bytesize data += buf break if data.include? crlf # crlf means line finished, the TCP socket keeps on listening, so we break end readline = data.chomp.split(" ") DBus.logger.debug "readline: #{readline.inspect}" readline end # # Read data (a buffer) from the bus until CR LF is encountered. # # Return the buffer without the CR LF characters. # def next_msg # @socket.readline.chomp.split(" ") # end # Try to reach the next state based on the current state. def next_state msg = next_msg if @state == :Starting DBus.logger.debug ":Starting msg: #{msg[0].inspect}" case msg[0] when "OK" @state = :WaitingForOk when "CONTINUE" @state = :WaitingForData when "REJECTED" # needed by tcp, unix-path/abstract doesn't get here @state = :WaitingForData end end DBus.logger.debug "state: #{@state}" case @state when :WaitingForData DBus.logger.debug ":WaitingForData msg: #{msg[0].inspect}" case msg[0] when "DATA" chall = msg[1] resp, chall = @authenticator.data(chall) DBus.logger.debug ":WaitingForData/DATA resp: #{resp.inspect}" case resp when :AuthContinue send("DATA", chall) @state = :WaitingForData when :AuthOk send("DATA", chall) @state = :WaitingForOk when :AuthError send("ERROR") @state = :WaitingForData end when "REJECTED" next_authenticator @state = :WaitingForData when "ERROR" send("CANCEL") @state = :WaitingForReject when "OK" send("BEGIN") @state = :Authenticated else send("ERROR") @state = :WaitingForData end when :WaitingForOk DBus.logger.debug ":WaitingForOk msg: #{msg[0].inspect}" case msg[0] when "OK" send("BEGIN") @state = :Authenticated when "REJECT" next_authenticator @state = :WaitingForData when "DATA", "ERROR" send("CANCEL") @state = :WaitingForReject else send("ERROR") @state = :WaitingForOk end when :WaitingForReject DBus.logger.debug ":WaitingForReject msg: #{msg[0].inspect}" case msg[0] when "REJECT" next_authenticator @state = :WaitingForOk else @socket.close return false end end true end # def next_state end # class Client end # module D-Bus