util/sasl.lua

2009-06-20

author
Tobias Markmann <tm@ayena.de>
date
Sat Jun 20 19:06:04 2009 +0200
changeset 1374
e85726d084d6
parent 1305
37657578ea85
child 1376
13587cf24435
permissions
-rw-r--r--

Adding COMPAT comment.

     1 -- sasl.lua v0.4
     2 -- Copyright (C) 2008-2009 Tobias Markmann
     3 -- 
     4 --    All rights reserved.
     5 --    
     6 --    Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
     7 --    
     8 --        * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
     9 --        * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
    10 --        * Neither the name of Tobias Markmann nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
    11 --    
    12 --    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
    15 local md5 = require "util.hashes".md5;
    16 local log = require "util.logger".init("sasl");
    17 local tostring = tostring;
    18 local st = require "util.stanza";
    19 local generate_uuid = require "util.uuid".generate;
    20 local t_insert, t_concat = table.insert, table.concat;
    21 local to_byte, to_char = string.byte, string.char;
    22 local s_match = string.match;
    23 local gmatch = string.gmatch
    24 local string = string
    25 local math = require "math"
    26 local type = type
    27 local error = error
    28 local print = print
    30 module "sasl"
    32 local function new_plain(realm, password_handler)
    33 	local object = { mechanism = "PLAIN", realm = realm, password_handler = password_handler}
    34 	function object.feed(self, message)
    36 		if message == "" or message == nil then return "failure", "malformed-request" end
    37 		local response = message
    38 		local authorization = s_match(response, "([^&%z]+)")
    39 		local authentication = s_match(response, "%z([^&%z]+)%z")
    40 		local password = s_match(response, "%z[^&%z]+%z([^&%z]+)")
    42 		if authentication == nil or password == nil then return "failure", "malformed-request" end
    44 		local password_encoding, correct_password = self.password_handler(authentication, self.realm, "PLAIN")
    46 		if correct_password == nil then return "failure", "not-authorized"
    47 		elseif correct_password == false then return "failure", "account-disabled" end
    49 		local claimed_password = ""
    50 		if password_encoding == nil then claimed_password = password
    51 		else claimed_password = password_encoding(password) end
    53 		self.username = authentication
    54 		if claimed_password == correct_password then
    55 			return "success"
    56 		else
    57 			return "failure", "not-authorized"
    58 		end
    59 	end
    60 	return object
    61 end
    64 -- implementing RFC 2831
    65 local function new_digest_md5(realm, password_handler)
    66 	--TODO complete support for authzid
    68 	local function serialize(message)
    69 		local data = ""
    71 		if type(message) ~= "table" then error("serialize needs an argument of type table.") end
    73 		-- testing all possible values
    74 		if message["nonce"] then data = data..[[nonce="]]..message.nonce..[[",]] end
    75 		if message["qop"] then data = data..[[qop="]]..message.qop..[[",]] end
    76 		if message["charset"] then data = data..[[charset=]]..message.charset.."," end
    77 		if message["algorithm"] then data = data..[[algorithm=]]..message.algorithm.."," end
    78 		if message["realm"] then data = data..[[realm="]]..message.realm..[[",]] end
    79 		if message["rspauth"] then data = data..[[rspauth=]]..message.rspauth.."," end
    80 		data = data:gsub(",$", "")
    81 		return data
    82 	end
    84 	local function utf8tolatin1ifpossible(passwd)
    85 		local i = 1;
    86 		while i <= #passwd do
    87 			local passwd_i = to_byte(passwd:sub(i, i));
    88 			if passwd_i > 0x7F then
    89 				if passwd_i < 0xC0 or passwd_i > 0xC3 then
    90 					return passwd;
    91 				end
    92 				i = i + 1;
    93 				passwd_i = to_byte(passwd:sub(i, i));
    94 				if passwd_i < 0x80 or passwd_i > 0xBF then
    95 					return passwd;
    96 				end
    97 			end
    98 			i = i + 1;
    99 		end
   101 		local p = {};
   102 		local j = 0;
   103 		i = 1;
   104 		while (i <= #passwd) do
   105 			local passwd_i = to_byte(passwd:sub(i, i));
   106 			if passwd_i > 0x7F then
   107 				i = i + 1;
   108 				local passwd_i_1 = to_byte(passwd:sub(i, i));
   109 				t_insert(p, to_char(passwd_i%4*64 + passwd_i_1%64)); -- I'm so clever
   110 			else
   111 				t_insert(p, to_char(passwd_i));
   112 			end
   113 			i = i + 1;
   114 		end
   115 		return t_concat(p);
   116 	end
   117 	local function latin1toutf8(str)
   118 		local p = {};
   119 		for ch in gmatch(str, ".") do
   120 			ch = to_byte(ch);
   121 			if (ch < 0x80) then
   122 				t_insert(p, to_char(ch));
   123 			elseif (ch < 0xC0) then
   124 				t_insert(p, to_char(0xC2, ch));
   125 			else
   126 				t_insert(p, to_char(0xC3, ch - 64));
   127 			end
   128 		end
   129 		return t_concat(p);
   130 	end
   131 	local function parse(data)
   132 		message = {}
   133 		for k, v in gmatch(data, [[([%w%-]+)="?([^",]*)"?,?]]) do -- FIXME The hacky regex makes me shudder
   134 			message[k] = v;
   135 		end
   136 		return message;
   137 	end
   139 	local object = { mechanism = "DIGEST-MD5", realm = realm, password_handler = password_handler};
   141 	object.nonce = generate_uuid();
   142 	object.step = 0;
   143 	object.nonce_count = {};
   145 	function object.feed(self, message)
   146 		self.step = self.step + 1;
   147 		if (self.step == 1) then
   148 			local challenge = serialize({	nonce = object.nonce, 
   149 											qop = "auth",
   150 											charset = "utf-8",
   151 											algorithm = "md5-sess",
   152 											realm = self.realm});
   153 			return "challenge", challenge;
   154 		elseif (self.step == 2) then
   155 			local response = parse(message);
   156 			-- check for replay attack
   157 			if response["nc"] then
   158 				if self.nonce_count[response["nc"]] then return "failure", "not-authorized" end
   159 			end
   161 			-- check for username, it's REQUIRED by RFC 2831
   162 			if not response["username"] then
   163 				return "failure", "malformed-request";
   164 			end
   165 			self["username"] = response["username"];
   167 			-- check for nonce, ...
   168 			if not response["nonce"] then
   169 				return "failure", "malformed-request";
   170 			else
   171 				-- check if it's the right nonce
   172 				if response["nonce"] ~= tostring(self.nonce) then return "failure", "malformed-request" end
   173 			end
   175 			if not response["cnonce"] then return "failure", "malformed-request", "Missing entry for cnonce in SASL message." end
   176 			if not response["qop"] then response["qop"] = "auth" end
   178 			if response["realm"] == nil or response["realm"] == "" then
   179 				response["realm"] = self.realm;
   180 			elseif response["realm"] ~= self.realm then
   181 				return "failure", "not-authorized", "Incorrect realm value";
   182 			end
   184 			local decoder;
   185 			if response["charset"] == nil then
   186 				decoder = utf8tolatin1ifpossible;
   187 			elseif response["charset"] ~= "utf-8" then
   188 				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.";
   189 			end
   191 			local domain = "";
   192 			local protocol = "";
   193 			if response["digest-uri"] then
   194 				protocol, domain = response["digest-uri"]:match("(%w+)/(.*)$");
   195 				if protocol == nil or domain == nil then return "failure", "malformed-request" end
   196 			else
   197 				return "failure", "malformed-request", "Missing entry for digest-uri in SASL message."
   198 			end
   200 			--TODO maybe realm support
   201 			self.username = response["username"];
   202 			local password_encoding, Y = self.password_handler(response["username"], response["realm"], "DIGEST-MD5", decoder)
   203 			if Y == nil then return "failure", "not-authorized"
   204 			elseif Y == false then return "failure", "account-disabled" end
   205 			local A1 = "";
   206 			if response.authzid then
   207 				if response.authzid == self.username.."@"..self.realm then
   208 					-- COMPAT
   209 					log("warn", "Client is violating XMPP RFC. See section 6.1 of RFC 3920.");
   210 					A1 = Y..":"..response["nonce"]..":"..response["cnonce"]..":"..response.authzid;
   211 				else
   212 					A1 = "?";
   213 				end
   214 			else
   215 				A1 = Y..":"..response["nonce"]..":"..response["cnonce"];
   216 			end
   217 			local A2 = "AUTHENTICATE:"..protocol.."/"..domain;
   219 			local HA1 = md5(A1, true);
   220 			local HA2 = md5(A2, true);
   222 			local KD = HA1..":"..response["nonce"]..":"..response["nc"]..":"..response["cnonce"]..":"..response["qop"]..":"..HA2;
   223 			local response_value = md5(KD, true);
   225 			if response_value == response["response"] then
   226 				-- calculate rspauth
   227 				A2 = ":"..protocol.."/"..domain;
   229 				HA1 = md5(A1, true);
   230 				HA2 = md5(A2, true);
   232 				KD = HA1..":"..response["nonce"]..":"..response["nc"]..":"..response["cnonce"]..":"..response["qop"]..":"..HA2
   233 				local rspauth = md5(KD, true);
   234 				self.authenticated = true;
   235 				return "challenge", serialize({rspauth = rspauth});
   236 			else
   237 				return "failure", "not-authorized", "The response provided by the client doesn't match the one we calculated."
   238 			end							
   239 		elseif self.step == 3 then
   240 			if self.authenticated ~= nil then return "success"
   241 			else return "failure", "malformed-request" end
   242 		end
   243 	end
   244 	return object;
   245 end
   247 local function new_anonymous(realm, password_handler)
   248 	local object = { mechanism = "ANONYMOUS", realm = realm, password_handler = password_handler}
   249 		function object.feed(self, message)
   250 			return "success"
   251 		end
   252 	object["username"] = generate_uuid()
   253 	return object
   254 end
   257 function new(mechanism, realm, password_handler)
   258 	local object
   259 	if mechanism == "PLAIN" then object = new_plain(realm, password_handler)
   260 	elseif mechanism == "DIGEST-MD5" then object = new_digest_md5(realm, password_handler)
   261 	elseif mechanism == "ANONYMOUS" then object = new_anonymous(realm, password_handler)
   262 	else
   263 		log("debug", "Unsupported SASL mechanism: "..tostring(mechanism));
   264 		return nil
   265 	end
   266 	return object
   267 end
   269 return _M;

mercurial