summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--lain.rb31
-rw-r--r--sasl.rb201
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 @@
+*~
diff --git a/lain.rb b/lain.rb
new file mode 100644
index 0000000..802d378
--- /dev/null
+++ b/lain.rb
@@ -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
diff --git a/sasl.rb b/sasl.rb
new file mode 100644
index 0000000..f871533
--- /dev/null
+++ b/sasl.rb
@@ -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