Importing SASL Digest-MD5 code. Now for real. sasl

Fri, 28 Aug 2009 19:57:09 +0200

author
Tobias Markmann <tm@ayena.de>
date
Fri, 28 Aug 2009 19:57:09 +0200
branch
sasl
changeset 2179
44e71e65da86
parent 2178
27c7d287345e
child 2180
0d1740f7b6e8

Importing SASL Digest-MD5 code. Now for real.

util/sasl.lua file | annotate | diff | comparison | revisions
--- a/util/sasl.lua	Fri Aug 28 19:56:54 2009 +0200
+++ b/util/sasl.lua	Fri Aug 28 19:57:09 2009 +0200
@@ -151,4 +151,186 @@
 
 --=========================
 --SASL DIGEST-MD5 according to RFC 2831
+local function new_digest_md5(realm, credentials_handler)
+	--TODO complete support for authzid
+
+	local function serialize(message)
+		local data = ""
+
+		if type(message) ~= "table" then error("serialize needs an argument of type table.") end
+
+		-- testing all possible values
+		if message["realm"] then data = data..[[realm="]]..message.realm..[[",]] end
+		if message["nonce"] then data = data..[[nonce="]]..message.nonce..[[",]] end
+		if message["qop"] then data = data..[[qop="]]..message.qop..[[",]] end
+		if message["charset"] then data = data..[[charset=]]..message.charset.."," end
+		if message["algorithm"] then data = data..[[algorithm=]]..message.algorithm.."," end
+		if message["rspauth"] then data = data..[[rspauth=]]..message.rspauth.."," end
+		data = data:gsub(",$", "")
+		return data
+	end
+
+	local function utf8tolatin1ifpossible(passwd)
+		local i = 1;
+		while i <= #passwd do
+			local passwd_i = to_byte(passwd:sub(i, i));
+			if passwd_i > 0x7F then
+				if passwd_i < 0xC0 or passwd_i > 0xC3 then
+					return passwd;
+				end
+				i = i + 1;
+				passwd_i = to_byte(passwd:sub(i, i));
+				if passwd_i < 0x80 or passwd_i > 0xBF then
+					return passwd;
+				end
+			end
+			i = i + 1;
+		end
+
+		local p = {};
+		local j = 0;
+		i = 1;
+		while (i <= #passwd) do
+			local passwd_i = to_byte(passwd:sub(i, i));
+			if passwd_i > 0x7F then
+				i = i + 1;
+				local passwd_i_1 = to_byte(passwd:sub(i, i));
+				t_insert(p, to_char(passwd_i%4*64 + passwd_i_1%64)); -- I'm so clever
+			else
+				t_insert(p, to_char(passwd_i));
+			end
+			i = i + 1;
+		end
+		return t_concat(p);
+	end
+	local function latin1toutf8(str)
+		local p = {};
+		for ch in gmatch(str, ".") do
+			ch = to_byte(ch);
+			if (ch < 0x80) then
+				t_insert(p, to_char(ch));
+			elseif (ch < 0xC0) then
+				t_insert(p, to_char(0xC2, ch));
+			else
+				t_insert(p, to_char(0xC3, ch - 64));
+			end
+		end
+		return t_concat(p);
+	end
+	local function parse(data)
+		local message = {}
+		for k, v in gmatch(data, [[([%w%-]+)="?([^",]*)"?,?]]) do -- FIXME The hacky regex makes me shudder
+			message[k] = v;
+		end
+		return message;
+	end
+
+	local object = { mechanism = "DIGEST-MD5", realm = realm, credentials_handler = credentials_handler};
+
+	object.nonce = generate_uuid();
+	object.step = 0;
+	object.nonce_count = {};
+
+	function object.feed(self, message)
+		self.step = self.step + 1;
+		if (self.step == 1) then
+			local challenge = serialize({	nonce = object.nonce,
+											qop = "auth",
+											charset = "utf-8",
+											algorithm = "md5-sess",
+											realm = self.realm});
+			return "challenge", challenge;
+		elseif (self.step == 2) then
+			local response = parse(message);
+			-- check for replay attack
+			if response["nc"] then
+				if self.nonce_count[response["nc"]] then return "failure", "not-authorized" end
+			end
+
+			-- check for username, it's REQUIRED by RFC 2831
+			if not response["username"] then
+				return "failure", "malformed-request";
+			end
+			self["username"] = response["username"];
+
+			-- check for nonce, ...
+			if not response["nonce"] then
+				return "failure", "malformed-request";
+			else
+				-- check if it's the right nonce
+				if response["nonce"] ~= tostring(self.nonce) then return "failure", "malformed-request" end
+			end
+
+			if not response["cnonce"] then return "failure", "malformed-request", "Missing entry for cnonce in SASL message." end
+			if not response["qop"] then response["qop"] = "auth" end
+
+			if response["realm"] == nil or response["realm"] == "" then
+				response["realm"] = "";
+			elseif response["realm"] ~= self.realm then
+				return "failure", "not-authorized", "Incorrect realm value";
+			end
+
+			local decoder;
+			if response["charset"] == nil then
+				decoder = utf8tolatin1ifpossible;
+			elseif response["charset"] ~= "utf-8" then
+				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.";
+			end
+
+			local domain = "";
+			local protocol = "";
+			if response["digest-uri"] then
+				protocol, domain = response["digest-uri"]:match("(%w+)/(.*)$");
+				if protocol == nil or domain == nil then return "failure", "malformed-request" end
+			else
+				return "failure", "malformed-request", "Missing entry for digest-uri in SASL message."
+			end
+
+			--TODO maybe realm support
+			self.username = response["username"];
+			local password_encoding, Y = self.credentials_handler("DIGEST-MD5", response["username"], self.realm, response["realm"], decoder);
+			if Y == nil then return "failure", "not-authorized"
+			elseif Y == false then return "failure", "account-disabled" end
+			local A1 = "";
+			if response.authzid then
+				if response.authzid == self.username.."@"..self.realm then
+					-- COMPAT
+					log("warn", "Client is violating XMPP RFC. See section 6.1 of RFC 3920.");
+					A1 = Y..":"..response["nonce"]..":"..response["cnonce"]..":"..response.authzid;
+				else
+					A1 = "?";
+				end
+			else
+				A1 = Y..":"..response["nonce"]..":"..response["cnonce"];
+			end
+			local A2 = "AUTHENTICATE:"..protocol.."/"..domain;
+
+			local HA1 = md5(A1, true);
+			local HA2 = md5(A2, true);
+
+			local KD = HA1..":"..response["nonce"]..":"..response["nc"]..":"..response["cnonce"]..":"..response["qop"]..":"..HA2;
+			local response_value = md5(KD, true);
+
+			if response_value == response["response"] then
+				-- calculate rspauth
+				A2 = ":"..protocol.."/"..domain;
+
+				HA1 = md5(A1, true);
+				HA2 = md5(A2, true);
+
+				KD = HA1..":"..response["nonce"]..":"..response["nc"]..":"..response["cnonce"]..":"..response["qop"]..":"..HA2
+				local rspauth = md5(KD, true);
+				self.authenticated = true;
+				return "challenge", serialize({rspauth = rspauth});
+			else
+				return "failure", "not-authorized", "The response provided by the client doesn't match the one we calculated."
+			end
+		elseif self.step == 3 then
+			if self.authenticated ~= nil then return "success"
+			else return "failure", "malformed-request" end
+		end
+	end
+	return object;
+end
+
 return _M;

mercurial