1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
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
|