149 end |
149 end |
150 registerMechanism("PLAIN", {"plain", "plain_test"}, sasl_mechanism_plain); |
150 registerMechanism("PLAIN", {"plain", "plain_test"}, sasl_mechanism_plain); |
151 |
151 |
152 --========================= |
152 --========================= |
153 --SASL DIGEST-MD5 according to RFC 2831 |
153 --SASL DIGEST-MD5 according to RFC 2831 |
|
154 local function new_digest_md5(realm, credentials_handler) |
|
155 --TODO complete support for authzid |
|
156 |
|
157 local function serialize(message) |
|
158 local data = "" |
|
159 |
|
160 if type(message) ~= "table" then error("serialize needs an argument of type table.") end |
|
161 |
|
162 -- testing all possible values |
|
163 if message["realm"] then data = data..[[realm="]]..message.realm..[[",]] end |
|
164 if message["nonce"] then data = data..[[nonce="]]..message.nonce..[[",]] end |
|
165 if message["qop"] then data = data..[[qop="]]..message.qop..[[",]] end |
|
166 if message["charset"] then data = data..[[charset=]]..message.charset.."," end |
|
167 if message["algorithm"] then data = data..[[algorithm=]]..message.algorithm.."," end |
|
168 if message["rspauth"] then data = data..[[rspauth=]]..message.rspauth.."," end |
|
169 data = data:gsub(",$", "") |
|
170 return data |
|
171 end |
|
172 |
|
173 local function utf8tolatin1ifpossible(passwd) |
|
174 local i = 1; |
|
175 while i <= #passwd do |
|
176 local passwd_i = to_byte(passwd:sub(i, i)); |
|
177 if passwd_i > 0x7F then |
|
178 if passwd_i < 0xC0 or passwd_i > 0xC3 then |
|
179 return passwd; |
|
180 end |
|
181 i = i + 1; |
|
182 passwd_i = to_byte(passwd:sub(i, i)); |
|
183 if passwd_i < 0x80 or passwd_i > 0xBF then |
|
184 return passwd; |
|
185 end |
|
186 end |
|
187 i = i + 1; |
|
188 end |
|
189 |
|
190 local p = {}; |
|
191 local j = 0; |
|
192 i = 1; |
|
193 while (i <= #passwd) do |
|
194 local passwd_i = to_byte(passwd:sub(i, i)); |
|
195 if passwd_i > 0x7F then |
|
196 i = i + 1; |
|
197 local passwd_i_1 = to_byte(passwd:sub(i, i)); |
|
198 t_insert(p, to_char(passwd_i%4*64 + passwd_i_1%64)); -- I'm so clever |
|
199 else |
|
200 t_insert(p, to_char(passwd_i)); |
|
201 end |
|
202 i = i + 1; |
|
203 end |
|
204 return t_concat(p); |
|
205 end |
|
206 local function latin1toutf8(str) |
|
207 local p = {}; |
|
208 for ch in gmatch(str, ".") do |
|
209 ch = to_byte(ch); |
|
210 if (ch < 0x80) then |
|
211 t_insert(p, to_char(ch)); |
|
212 elseif (ch < 0xC0) then |
|
213 t_insert(p, to_char(0xC2, ch)); |
|
214 else |
|
215 t_insert(p, to_char(0xC3, ch - 64)); |
|
216 end |
|
217 end |
|
218 return t_concat(p); |
|
219 end |
|
220 local function parse(data) |
|
221 local message = {} |
|
222 for k, v in gmatch(data, [[([%w%-]+)="?([^",]*)"?,?]]) do -- FIXME The hacky regex makes me shudder |
|
223 message[k] = v; |
|
224 end |
|
225 return message; |
|
226 end |
|
227 |
|
228 local object = { mechanism = "DIGEST-MD5", realm = realm, credentials_handler = credentials_handler}; |
|
229 |
|
230 object.nonce = generate_uuid(); |
|
231 object.step = 0; |
|
232 object.nonce_count = {}; |
|
233 |
|
234 function object.feed(self, message) |
|
235 self.step = self.step + 1; |
|
236 if (self.step == 1) then |
|
237 local challenge = serialize({ nonce = object.nonce, |
|
238 qop = "auth", |
|
239 charset = "utf-8", |
|
240 algorithm = "md5-sess", |
|
241 realm = self.realm}); |
|
242 return "challenge", challenge; |
|
243 elseif (self.step == 2) then |
|
244 local response = parse(message); |
|
245 -- check for replay attack |
|
246 if response["nc"] then |
|
247 if self.nonce_count[response["nc"]] then return "failure", "not-authorized" end |
|
248 end |
|
249 |
|
250 -- check for username, it's REQUIRED by RFC 2831 |
|
251 if not response["username"] then |
|
252 return "failure", "malformed-request"; |
|
253 end |
|
254 self["username"] = response["username"]; |
|
255 |
|
256 -- check for nonce, ... |
|
257 if not response["nonce"] then |
|
258 return "failure", "malformed-request"; |
|
259 else |
|
260 -- check if it's the right nonce |
|
261 if response["nonce"] ~= tostring(self.nonce) then return "failure", "malformed-request" end |
|
262 end |
|
263 |
|
264 if not response["cnonce"] then return "failure", "malformed-request", "Missing entry for cnonce in SASL message." end |
|
265 if not response["qop"] then response["qop"] = "auth" end |
|
266 |
|
267 if response["realm"] == nil or response["realm"] == "" then |
|
268 response["realm"] = ""; |
|
269 elseif response["realm"] ~= self.realm then |
|
270 return "failure", "not-authorized", "Incorrect realm value"; |
|
271 end |
|
272 |
|
273 local decoder; |
|
274 if response["charset"] == nil then |
|
275 decoder = utf8tolatin1ifpossible; |
|
276 elseif response["charset"] ~= "utf-8" then |
|
277 return "failure", "incorrect-encoding", "The client's response uses "..response["charset"].." for encoding with isn't supported by sasl.lua. Supported encodings are latin or utf-8."; |
|
278 end |
|
279 |
|
280 local domain = ""; |
|
281 local protocol = ""; |
|
282 if response["digest-uri"] then |
|
283 protocol, domain = response["digest-uri"]:match("(%w+)/(.*)$"); |
|
284 if protocol == nil or domain == nil then return "failure", "malformed-request" end |
|
285 else |
|
286 return "failure", "malformed-request", "Missing entry for digest-uri in SASL message." |
|
287 end |
|
288 |
|
289 --TODO maybe realm support |
|
290 self.username = response["username"]; |
|
291 local password_encoding, Y = self.credentials_handler("DIGEST-MD5", response["username"], self.realm, response["realm"], decoder); |
|
292 if Y == nil then return "failure", "not-authorized" |
|
293 elseif Y == false then return "failure", "account-disabled" end |
|
294 local A1 = ""; |
|
295 if response.authzid then |
|
296 if response.authzid == self.username.."@"..self.realm then |
|
297 -- COMPAT |
|
298 log("warn", "Client is violating XMPP RFC. See section 6.1 of RFC 3920."); |
|
299 A1 = Y..":"..response["nonce"]..":"..response["cnonce"]..":"..response.authzid; |
|
300 else |
|
301 A1 = "?"; |
|
302 end |
|
303 else |
|
304 A1 = Y..":"..response["nonce"]..":"..response["cnonce"]; |
|
305 end |
|
306 local A2 = "AUTHENTICATE:"..protocol.."/"..domain; |
|
307 |
|
308 local HA1 = md5(A1, true); |
|
309 local HA2 = md5(A2, true); |
|
310 |
|
311 local KD = HA1..":"..response["nonce"]..":"..response["nc"]..":"..response["cnonce"]..":"..response["qop"]..":"..HA2; |
|
312 local response_value = md5(KD, true); |
|
313 |
|
314 if response_value == response["response"] then |
|
315 -- calculate rspauth |
|
316 A2 = ":"..protocol.."/"..domain; |
|
317 |
|
318 HA1 = md5(A1, true); |
|
319 HA2 = md5(A2, true); |
|
320 |
|
321 KD = HA1..":"..response["nonce"]..":"..response["nc"]..":"..response["cnonce"]..":"..response["qop"]..":"..HA2 |
|
322 local rspauth = md5(KD, true); |
|
323 self.authenticated = true; |
|
324 return "challenge", serialize({rspauth = rspauth}); |
|
325 else |
|
326 return "failure", "not-authorized", "The response provided by the client doesn't match the one we calculated." |
|
327 end |
|
328 elseif self.step == 3 then |
|
329 if self.authenticated ~= nil then return "success" |
|
330 else return "failure", "malformed-request" end |
|
331 end |
|
332 end |
|
333 return object; |
|
334 end |
|
335 |
154 return _M; |
336 return _M; |