util/sasl.lua

branch
sasl
changeset 2179
44e71e65da86
parent 2178
27c7d287345e
child 2180
0d1740f7b6e8
equal deleted inserted replaced
2178:27c7d287345e 2179:44e71e65da86
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;

mercurial