diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | lain.rb | 31 | ||||
-rw-r--r-- | sasl.rb | 201 |
3 files changed, 233 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b25c15b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*~ @@ -0,0 +1,31 @@ +#!/usr/bin/env ruby + + +require 'xmpp4r' +require 'xmpp4r/muc/helper/simplemucclient' + +require_relative 'sasl' + +require_relative 'config' + +include Jabber + + +cl = Jabber::Client.new(Jabber::JID.new(LainConfig::JID)) +cl.connect +cl.auth(LainConfig::Password) +cl.send(Presence.new) + +mainthread = Thread.current + +m = Jabber::MUC::MUCClient.new(cl) + +m.add_message_callback { |x| + puts x +} + +LainConfig::Rooms.each { |r| m.join("#{r}/#{LainConfig::Nick}") } + +Thread.stop + +cl.close @@ -0,0 +1,201 @@ +require 'digest/sha1' +require 'openssl' +require 'xmpp4r/client' +require 'xmpp4r/sasl' + + +module Jabber + class Client + def auth(password) + #begin + if @stream_mechanisms.include? 'SCRAM-SHA-1' + auth_sasl SASL.new(self, 'SCRAM-SHA-1'), password + elsif @stream_mechanisms.include? 'DIGEST-MD5' + auth_sasl SASL.new(self, 'DIGEST-MD5'), password + elsif @stream_mechanisms.include? 'PLAIN' + auth_sasl SASL.new(self, 'PLAIN'), password + else + auth_nonsasl(password) + end + #rescue + # Jabber::debuglog("#{$!.class}: #{$!}\n#{$!.backtrace.join("\n")}") + # raise ClientAuthenticationFailure.new, $!.to_s + #end + end + end + + module SASL + ## + # Factory function to obtain a SASL helper for the specified mechanism + def SASL.new(stream, mechanism) + case mechanism + when 'SCRAM-SHA-1' + SCRAMSHA1.new(stream) + when 'DIGEST-MD5' + DigestMD5.new(stream) + when 'PLAIN' + Plain.new(stream) + when 'ANONYMOUS' + Anonymous.new(stream) + else + raise "Unknown SASL mechanism: #{mechanism}" + end + end + + ## + # SASL SCRAM-SHA1 authentication helper + class SCRAMSHA1 < Base + ## + # Sends the wished auth mechanism and wait for a challenge + # + # (proceed with SCRAMSHA1#auth) + def initialize(stream) + super + + @nonce = generate_nonce + @client_fm = "n=#{escape @stream.jid.node },r=#{@nonce}" + + challenge = {} + challenge_text = '' + error = nil + @stream.send(generate_auth('SCRAM-SHA-1', text=Base64::strict_encode64('n,,'+@client_fm))) { |reply| + if reply.name == 'challenge' and reply.namespace == NS_SASL + challenge_text = Base64::decode64(reply.text) + challenge = decode_challenge(challenge_text) + else + error = reply.first_element(nil).name + end + true + } + raise error if error + + @server_fm = challenge_text + @cnonce = challenge['r'] + @salt = Base64::decode64(challenge['s']) + @iterations = challenge['i'].to_i + + raise 'SCRAM-SHA-1 protocol error' if @cnonce[0, @nonce.length] != @nonce + end + + def decode_challenge(text) + res = {} + + state = :key + key = '' + value = '' + text.scan(/./) do |ch| + if state == :key + if ch == '=' + state = :value + else + key += ch + end + + elsif state == :value + if ch == ',' + # due to our home-made parsing of the challenge, the key could have + # leading whitespace. strip it, or that would break jabberd2 support. + key = key.strip + res[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 + # due to our home-made parsing of the challenge, the key could have + # leading whitespace. strip it, or that would break jabberd2 support. + key = key.strip + res[key] = value unless key == '' + + Jabber::debuglog("SASL SCRAM-SHA-1 challenge:\n#{text}\n#{res.inspect}") + + res + end + + ## + # * Send a response + # * Wait for the server's challenge (which aren't checked) + # * Send a blind response to the server's challenge + def auth(password) + salted_password = hi(password, @salt, @iterations) + client_key = hmac(salted_password, 'Client Key') + stored_key = h(client_key) + + final_message = "c=#{Base64::strict_encode64('n,,')},r=#{@cnonce}" + auth_message = "#{@client_fm},#{@server_fm},#{final_message}" + + client_signature = hmac(stored_key, auth_message) + client_proof = xor(client_key, client_signature) + + + response_text = "#{final_message},p=#{Base64::strict_encode64(client_proof)}" + + Jabber::debuglog("SASL SCRAM-SHA-1 response:\n#{response_text}") + + r = REXML::Element.new('response') + r.add_namespace NS_SASL + r.text = Base64::strict_encode64(response_text) + + error = nil + success = {} + @stream.send(r) { |reply| + if reply.name == 'success' and reply.namespace == NS_SASL + success = decode_challenge(Base64::decode64(reply.text)) + elsif reply.name != 'challenge' + error = reply.first_element(nil).name + end + true + } + + raise error if error + + server_key = hmac(salted_password, 'Server Key') + server_signature = hmac(server_key, auth_message) + + raise "Server authentication failed" if Base64::decode64(success['v']) != server_signature + end + + private + + def xor(a, b) + a.unpack('C*').zip(b.unpack('C*')).collect { | x, y | x ^ y }.pack('C*') + end + + def h(s) + Digest::SHA1.digest(s) + end + + def hmac(key, s) + OpenSSL::HMAC.digest('sha1', key, s) + end + + def hi(s, salt, i) + r = Array.new(size=20, obj=0).pack('C*') + u = salt + [0, 0, 0, 1].pack('C*') + + i.times do |x| + u = hmac(s, u) + r = xor(r, u) + end + + r + end + + def escape(data) + data.gsub(/=/, '=3D').gsub(/,/, '=2C') + end + end + end +end |