pyu-ruby-sasl-0.0.3.3/0000755000175000017500000000000012132751305013732 5ustar ondrejondrejpyu-ruby-sasl-0.0.3.3/spec/0000755000175000017500000000000012132751305014664 5ustar ondrejondrejpyu-ruby-sasl-0.0.3.3/spec/digest_md5_spec.rb0000644000175000017500000000754412132751305020261 0ustar ondrejondrejrequire 'sasl' require 'spec' describe SASL::DigestMD5 do # Preferences from http://tools.ietf.org/html/rfc2831#section-4 class MyDigestMD5Preferences < SASL::Preferences attr_writer :serv_type def realm 'elwood.innosoft.com' end def digest_uri "#{@serv_type}/elwood.innosoft.com" end def username 'chris' end def has_password? true end def password 'secret' end end preferences = MyDigestMD5Preferences.new it 'should authenticate (1)' do preferences.serv_type = 'imap' sasl = SASL::DigestMD5.new('DIGEST-MD5', preferences) sasl.start.should == ['auth', nil] sasl.cnonce = 'OA6MHXh6VqTrRk' response = sasl.receive('challenge', 'realm="elwood.innosoft.com",nonce="OA6MG9tEQGm2hh",qop="auth", algorithm=md5-sess,charset=utf-8') response[0].should == 'response' response[1].should =~ /charset="?utf-8"?/ response[1].should =~ /username="?chris"?/ response[1].should =~ /realm="?elwood.innosoft.com"?/ response[1].should =~ /nonce="?OA6MG9tEQGm2hh"?/ response[1].should =~ /nc="?00000001"?/ response[1].should =~ /cnonce="?OA6MHXh6VqTrRk"?/ response[1].should =~ /digest-uri="?imap\/elwood.innosoft.com"?/ response[1].should =~ /response=d388dad90d4bbd760a152321f2143af7"?/ response[1].should =~ /"?qop=auth"?/ sasl.receive('challenge', 'rspauth=ea40f60335c427b5527b84dbabcdfffd').should == ['response', nil] sasl.receive('success', nil).should == nil sasl.success?.should == true end it 'should authenticate (2)' do preferences.serv_type = 'acap' sasl = SASL::DigestMD5.new('DIGEST-MD5', preferences) sasl.start.should == ['auth', nil] sasl.cnonce = 'OA9BSuZWMSpW8m' response = sasl.receive('challenge', 'realm="elwood.innosoft.com",nonce="OA9BSXrbuRhWay",qop="auth", algorithm=md5-sess,charset=utf-8') response[0].should == 'response' response[1].should =~ /charset="?utf-8"?/ response[1].should =~ /username="?chris"?/ response[1].should =~ /realm="?elwood.innosoft.com"?/ response[1].should =~ /nonce="?OA9BSXrbuRhWay"?/ response[1].should =~ /nc="?00000001"?/ response[1].should =~ /cnonce="?OA9BSuZWMSpW8m"?/ response[1].should =~ /digest-uri="?acap\/elwood.innosoft.com"?/ response[1].should =~ /response=6084c6db3fede7352c551284490fd0fc"?/ response[1].should =~ /"?qop=auth"?/ sasl.receive('challenge', 'rspauth=2f0b3d7c3c2e486600ef710726aa2eae').should == ['response', nil] sasl.receive('success', nil).should == nil sasl.success?.should == true end it 'should reauthenticate' do preferences.serv_type = 'imap' sasl = SASL::DigestMD5.new('DIGEST-MD5', preferences) sasl.start.should == ['auth', nil] sasl.cnonce = 'OA6MHXh6VqTrRk' sasl.receive('challenge', 'realm="elwood.innosoft.com",nonce="OA6MG9tEQGm2hh",qop="auth", algorithm=md5-sess,charset=utf-8') # reauth: response = sasl.start response[0].should == 'response' response[1].should =~ /charset="?utf-8"?/ response[1].should =~ /username="?chris"?/ response[1].should =~ /realm="?elwood.innosoft.com"?/ response[1].should =~ /nonce="?OA6MG9tEQGm2hh"?/ response[1].should =~ /nc="?00000002"?/ response[1].should =~ /cnonce="?OA6MHXh6VqTrRk"?/ response[1].should =~ /digest-uri="?imap\/elwood.innosoft.com"?/ response[1].should =~ /response=b0b5d72a400655b8306e434566b10efb"?/ # my own result response[1].should =~ /"?qop=auth"?/ end it 'should fail' do sasl = SASL::DigestMD5.new('DIGEST-MD5', preferences) sasl.start.should == ['auth', nil] sasl.receive('failure', 'EPIC FAIL') sasl.failure?.should == true sasl.success?.should == false end end pyu-ruby-sasl-0.0.3.3/spec/mechanism_spec.rb0000644000175000017500000000306112132751305020167 0ustar ondrejondrejrequire 'sasl' require 'spec' describe SASL do it 'should know DIGEST-MD5' do sasl = SASL.new_mechanism('DIGEST-MD5', SASL::Preferences.new) sasl.should be_an_instance_of SASL::DigestMD5 end it 'should know PLAIN' do sasl = SASL.new_mechanism('PLAIN', SASL::Preferences.new) sasl.should be_an_instance_of SASL::Plain end it 'should know ANONYMOUS' do sasl = SASL.new_mechanism('ANONYMOUS', SASL::Preferences.new) sasl.should be_an_instance_of SASL::Anonymous end it 'should choose ANONYMOUS' do preferences = SASL::Preferences.new class << preferences def want_anonymous? true end end SASL.new(%w(PLAIN DIGEST-MD5 ANONYMOUS), preferences).should be_an_instance_of SASL::Anonymous end it 'should choose DIGEST-MD5' do preferences = SASL::Preferences.new class << preferences def has_password? true end end SASL.new(%w(PLAIN DIGEST-MD5 ANONYMOUS), preferences).should be_an_instance_of SASL::DigestMD5 end it 'should choose PLAIN' do preferences = SASL::Preferences.new class << preferences def has_password? true end def allow_plaintext? true end end SASL.new(%w(PLAIN ANONYMOUS), preferences).should be_an_instance_of SASL::Plain end it 'should disallow PLAIN by default' do preferences = SASL::Preferences.new class << preferences def has_password? true end end lambda { SASL.new(%w(PLAIN ANONYMOUS), preferences) }.should raise_error(SASL::UnknownMechanism) end end pyu-ruby-sasl-0.0.3.3/spec/plain_spec.rb0000644000175000017500000000172212132751305017330 0ustar ondrejondrejrequire 'sasl' require 'spec' describe SASL::Plain do class MyPlainPreferences < SASL::Preferences def authzid 'bob@example.com' end def username 'bob' end def has_password? true end def password 's3cr3t' end end preferences = MyPlainPreferences.new it 'should authenticate' do sasl = SASL::Plain.new('PLAIN', preferences) sasl.start.should == ['auth', "bob@example.com\000bob\000s3cr3t"] sasl.success?.should == false sasl.receive('success', nil).should == nil sasl.failure?.should == false sasl.success?.should == true end it 'should recognize failure' do sasl = SASL::Plain.new('PLAIN', preferences) sasl.start.should == ['auth', "bob@example.com\000bob\000s3cr3t"] sasl.success?.should == false sasl.failure?.should == false sasl.receive('failure', 'keep-idiots-out').should == nil sasl.failure?.should == true sasl.success?.should == false end end pyu-ruby-sasl-0.0.3.3/spec/anonymous_spec.rb0000644000175000017500000000071512132751305020256 0ustar ondrejondrejrequire 'sasl' require 'spec' describe SASL::Anonymous do class MyAnonymousPreferences < SASL::Preferences def username 'bob' end end preferences = MyAnonymousPreferences.new it 'should authenticate anonymously' do sasl = SASL::Anonymous.new('ANONYMOUS', preferences) sasl.start.should == ['auth', 'bob'] sasl.success?.should == false sasl.receive('success', nil).should == nil sasl.success?.should == true end end pyu-ruby-sasl-0.0.3.3/lib/0000755000175000017500000000000012132751305014500 5ustar ondrejondrejpyu-ruby-sasl-0.0.3.3/lib/sasl.rb0000644000175000017500000000025312132751305015767 0ustar ondrejondrejSASL_PATH = File.dirname(__FILE__) + "/sasl" require 'sasl/base' Dir.foreach(SASL_PATH) do |f| require "#{SASL_PATH}/#{f}" if f =~ /^[^\.].+\.rb$/ && f != 'base.rb' end pyu-ruby-sasl-0.0.3.3/lib/sasl/0000755000175000017500000000000012132751305015442 5ustar ondrejondrejpyu-ruby-sasl-0.0.3.3/lib/sasl/plain.rb0000644000175000017500000000046112132751305017073 0ustar ondrejondrejmodule SASL ## # RFC 4616: # http://tools.ietf.org/html/rfc4616 class Plain < Mechanism def start @state = nil message = [preferences.authzid.to_s, preferences.username, preferences.password].join("\000") ['auth', message] end end end pyu-ruby-sasl-0.0.3.3/lib/sasl/base.rb0000644000175000017500000000633212132751305016705 0ustar ondrejondrej## # RFC 4422: # http://tools.ietf.org/html/rfc4422 module SASL ## # You must derive from class Preferences and overwrite methods you # want to implement. class Preferences attr_reader :config # key in config hash # authzid: Authorization identitiy ('username@domain' in XMPP) # realm: Realm ('domain' in XMPP) # digest-uri: : serv-type/serv-name | serv-type/host/serv-name ('xmpp/domain' in XMPP) # username # has_password? # allow_plaintext? # password # want_anonymous? def initialize (config) @config = {:has_password? => false, :allow_plaintext? => false, :want_anonymous? => false}.merge(config.dup) end def method_missing(sym, *args, &block) @config.send "[]", sym, &block end end ## # Will be raised by SASL.new_mechanism if mechanism passed to the # constructor is not known. class UnknownMechanism < RuntimeError def initialize(mechanism) @mechanism = mechanism end def to_s "Unknown mechanism: #{@mechanism.inspect}" end end def SASL.new(mechanisms, preferences) best_mechanism = if preferences.want_anonymous? && mechanisms.include?('ANONYMOUS') 'ANONYMOUS' elsif preferences.has_password? if mechanisms.include?('DIGEST-MD5') 'DIGEST-MD5' elsif preferences.allow_plaintext? 'PLAIN' else raise UnknownMechanism.new(mechanisms) end else raise UnknownMechanism.new(mechanisms) end new_mechanism(best_mechanism, preferences) end ## # Create a SASL Mechanism for the named mechanism # # mechanism:: [String] mechanism name def SASL.new_mechanism(mechanism, preferences) mechanism_class = case mechanism when 'DIGEST-MD5' DigestMD5 when 'PLAIN' Plain when 'ANONYMOUS' Anonymous else raise UnknownMechanism.new(mechanism) end mechanism_class.new(mechanism, preferences) end class AbstractMethod < Exception # :nodoc: def to_s "Abstract method is not implemented" end end ## # Common functions for mechanisms # # Mechanisms implement handling of methods start and receive. They # return: [message_name, content] or nil where message_name is # either 'auth' or 'response' and content is either a string which # may transmitted encoded as Base64 or nil. class Mechanism attr_reader :mechanism attr_reader :preferences def initialize(mechanism, preferences) @mechanism = mechanism @preferences = preferences @state = nil end def success? @state == :success end def failure? @state == :failure end def start raise AbstractMethod end def receive(message_name, content) case message_name when 'success' @state = :success when 'failure' @state = :failure end nil end end end pyu-ruby-sasl-0.0.3.3/lib/sasl/digest_md5.rb0000644000175000017500000001055012132751305020014 0ustar ondrejondrejrequire 'digest/md5' module SASL ## # RFC 2831: # http://tools.ietf.org/html/rfc2831 class DigestMD5 < Mechanism attr_writer :cnonce def initialize(*a) super @nonce_count = 0 end def start @state = nil unless defined? @nonce ['auth', nil] else # reauthentication receive('challenge', '') end end def receive(message_name, content) if message_name == 'challenge' c = decode_challenge(content) unless c['rspauth'] response = {} if defined?(@nonce) && response['nonce'].nil? # Could be reauth else # No reauth: @nonce_count = 0 end @nonce ||= c['nonce'] response['nonce'] = @nonce response['charset'] = 'utf-8' response['username'] = preferences.username response['realm'] = c['realm'] || preferences.realm @cnonce = generate_nonce unless defined? @cnonce response['cnonce'] = @cnonce @nc = next_nc response['nc'] = @nc @qop = c['qop'] || 'auth' response['qop'] = 'auth' #@qop response['digest-uri'] = preferences.digest_uri response['response'] = response_value(response['nonce'], response['nc'], response['cnonce'], response['qop'], response['realm']) ['response', encode_response(response)] else rspauth_expected = response_value(@nonce, @nc, @cnonce, @qop, '') #p :rspauth_received=>c['rspauth'], :rspauth_expected=>rspauth_expected if c['rspauth'] == rspauth_expected ['response', nil] else # Bogus server? @state = :failure ['failure', nil] end end else # No challenge? Might be success or failure super end end private def decode_challenge(text) challenge = {} state = :key key = '' value = '' text.scan(/./) do |ch| if state == :key if ch == '=' state = :value elsif ch =~ /\S/ key += ch end elsif state == :value if ch == ',' challenge[key] = value key = '' value = '' state = :key elsif ch == '"' and value == '' state = :quote else value += ch end elsif state == :quote if ch == '"' state = :value else value += ch end end end challenge[key] = value unless key == '' #p :decode_challenge => challenge challenge end def encode_response(response) #p :encode_response => response response.collect do |k,v| if ['username', 'cnonce', 'digest-uri', 'authzid','realm','qop'].include? k v.sub!('\\', '\\\\') v.sub!('"', '\\"') "#{k}=\"#{v}\"" else "#{k}=#{v}" end end.join(',') end def generate_nonce nonce = '' while nonce.length < 32 c = rand(128).chr nonce += c if c =~ /^[a-zA-Z0-9]$/ end nonce end ## # Function from RFC2831 def h(s); Digest::MD5.digest(s); end ## # Function from RFC2831 def hh(s); Digest::MD5.hexdigest(s); end ## # Calculate the value for the response field def response_value(nonce, nc, cnonce, qop, realm, a2_prefix='AUTHENTICATE') #p :response_value => {:nonce=>nonce, # :cnonce=>cnonce, # :qop=>qop, # :username=>preferences.username, # :realm=>preferences.realm, # :password=>preferences.password, # :authzid=>preferences.authzid} a1_h = h("#{preferences.username}:#{realm}:#{preferences.password}") a1 = "#{a1_h}:#{nonce}:#{cnonce}" if preferences.authzid a1 += ":#{preferences.authzid}" end if qop && (qop.downcase == 'auth-int' || qop.downcase == 'auth-conf') a2 = "#{a2_prefix}:#{preferences.digest_uri}:00000000000000000000000000000000" else a2 = "#{a2_prefix}:#{preferences.digest_uri}" end hh("#{hh(a1)}:#{nonce}:#{nc}:#{cnonce}:#{qop}:#{hh(a2)}") end def next_nc @nonce_count += 1 s = @nonce_count.to_s s = "0#{s}" while s.length < 8 s end end end pyu-ruby-sasl-0.0.3.3/lib/sasl/anonymous.rb0000644000175000017500000000044512132751305020022 0ustar ondrejondrejmodule SASL ## # SASL ANONYMOUS where you only send a username that may not get # evaluated by the server. # # RFC 4505: # http://tools.ietf.org/html/rfc4505 class Anonymous < Mechanism def start @state = nil ['auth', preferences.username.to_s] end end end pyu-ruby-sasl-0.0.3.3/lib/sasl/base64.rb0000644000175000017500000000137212132751305017056 0ustar ondrejondrej# =XMPP4R - XMPP Library for Ruby # License:: Ruby's license (see the LICENSE file) or GNU GPL, at your option. # Website::http://home.gna.org/xmpp4r/ begin require 'base64' rescue LoadError ## # Ruby 1.9 has dropped the Base64 module, # this is a replacement # # We could replace all call by Array#pack('m') # and String#unpack('m'), but this module # improves readability. module Base64 ## # Encode a String # data:: [String] Binary # result:: [String] Binary in Base64 def self.encode64(data) [data].pack('m') end ## # Decode a Base64-encoded String # data64:: [String] Binary in Base64 # result:: [String] Binary def self.decode64(data64) data64.unpack('m').first end end end pyu-ruby-sasl-0.0.3.3/README.markdown0000644000175000017500000000123512132751305016434 0ustar ondrejondrejSimple Authentication and Security Layer (RFC 4422) for Ruby ============================================================ Goal ---- Have a reusable library for client implementations that need to do authentication over SASL, mainly targeted at Jabber/XMPP libraries. All class carry just state, are thread-agnostic and must also work in asynchronous environments. Usage ----- Derive from **SASL::Preferences** and overwrite the methods. Then, create a mechanism instance: # mechanisms => ['DIGEST-MD5', 'PLAIN'] sasl = SASL.new(mechanisms, my_preferences) content_to_send = sasl.start # [...] content_to_send = sasl.challenge(received_content) pyu-ruby-sasl-0.0.3.3/metadata.yml0000644000175000017500000000252112132751305016235 0ustar ondrejondrej--- !ruby/object:Gem::Specification name: pyu-ruby-sasl version: !ruby/object:Gem::Version prerelease: version: 0.0.3.3 platform: ruby authors: - Stephan Maka - Ping Yu autorequire: bindir: bin cert_chain: [] date: 2010-10-18 00:00:00 -05:00 default_executable: dependencies: [] description: Simple Authentication and Security Layer (RFC 4422) email: pyu@intridea.com executables: [] extensions: [] extra_rdoc_files: [] files: - spec/mechanism_spec.rb - spec/anonymous_spec.rb - spec/plain_spec.rb - spec/digest_md5_spec.rb - lib/sasl/base.rb - lib/sasl/digest_md5.rb - lib/sasl/anonymous.rb - lib/sasl/plain.rb - lib/sasl/base64.rb - lib/sasl.rb - README.markdown has_rdoc: true homepage: http://github.com/pyu10055/ruby-sasl/ licenses: [] post_install_message: rdoc_options: [] require_paths: - lib required_ruby_version: !ruby/object:Gem::Requirement none: false requirements: - - ">=" - !ruby/object:Gem::Version version: "0" required_rubygems_version: !ruby/object:Gem::Requirement none: false requirements: - - ">=" - !ruby/object:Gem::Version version: "0" requirements: [] rubyforge_project: rubygems_version: 1.6.2 signing_key: specification_version: 3 summary: SASL client library test_files: - spec/mechanism_spec.rb - spec/anonymous_spec.rb - spec/plain_spec.rb - spec/digest_md5_spec.rb