2 -- Copyright (C) 2008-2009 Tobias Markmann
4 -- All rights reserved.
6 -- Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
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.
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.
14 local tostring = tostring;
17 local s_gmatch = string.gmatch;
18 local s_match = string.match;
19 local t_concat = table.concat;
20 local t_insert = table.insert;
21 local to_byte, to_char = string.byte, string.char;
23 local md5 = require "util.hashes".md5;
24 local log = require "util.logger".init("sasl");
25 local generate_uuid = require "util.uuid".generate;
29 --=========================
30 --SASL DIGEST-MD5 according to RFC 2831
32 local function digest(self, message)
33 --TODO complete support for authzid
35 local function serialize(message)
38 -- testing all possible values
39 if message["realm"] then data = data..[[realm="]]..message.realm..[[",]] end
40 if message["nonce"] then data = data..[[nonce="]]..message.nonce..[[",]] end
41 if message["qop"] then data = data..[[qop="]]..message.qop..[[",]] end
42 if message["charset"] then data = data..[[charset=]]..message.charset.."," end
43 if message["algorithm"] then data = data..[[algorithm=]]..message.algorithm.."," end
44 if message["rspauth"] then data = data..[[rspauth=]]..message.rspauth.."," end
45 data = data:gsub(",$", "")
49 local function utf8tolatin1ifpossible(passwd)
52 local passwd_i = to_byte(passwd:sub(i, i));
53 if passwd_i > 0x7F then
54 if passwd_i < 0xC0 or passwd_i > 0xC3 then
58 passwd_i = to_byte(passwd:sub(i, i));
59 if passwd_i < 0x80 or passwd_i > 0xBF then
69 while (i <= #passwd) do
70 local passwd_i = to_byte(passwd:sub(i, i));
71 if passwd_i > 0x7F then
73 local passwd_i_1 = to_byte(passwd:sub(i, i));
74 t_insert(p, to_char(passwd_i%4*64 + passwd_i_1%64)); -- I'm so clever
76 t_insert(p, to_char(passwd_i));
82 local function latin1toutf8(str)
84 for ch in s_gmatch(str, ".") do
87 t_insert(p, to_char(ch));
88 elseif (ch < 0xC0) then
89 t_insert(p, to_char(0xC2, ch));
91 t_insert(p, to_char(0xC3, ch - 64));
96 local function parse(data)
98 -- COMPAT: %z in the pattern to work around jwchat bug (sends "charset=utf-8\0")
99 for k, v in s_gmatch(data, [[([%w%-]+)="?([^",%z]*)"?,?]]) do -- FIXME The hacky regex makes me shudder
105 if not self.nonce then
106 self.nonce = generate_uuid();
108 self.nonce_count = {};
111 self.step = self.step + 1;
112 if (self.step == 1) then
113 local challenge = serialize({ nonce = self.nonce,
116 algorithm = "md5-sess",
117 realm = self.realm});
118 return "challenge", challenge;
119 elseif (self.step == 2) then
120 local response = parse(message);
121 -- check for replay attack
122 if response["nc"] then
123 if self.nonce_count[response["nc"]] then return "failure", "not-authorized" end
126 -- check for username, it's REQUIRED by RFC 2831
127 if not response["username"] then
128 return "failure", "malformed-request";
130 self["username"] = response["username"];
132 -- check for nonce, ...
133 if not response["nonce"] then
134 return "failure", "malformed-request";
136 -- check if it's the right nonce
137 if response["nonce"] ~= tostring(self.nonce) then return "failure", "malformed-request" end
140 if not response["cnonce"] then return "failure", "malformed-request", "Missing entry for cnonce in SASL message." end
141 if not response["qop"] then response["qop"] = "auth" end
143 if response["realm"] == nil or response["realm"] == "" then
144 response["realm"] = "";
145 elseif response["realm"] ~= self.realm then
146 return "failure", "not-authorized", "Incorrect realm value";
150 if response["charset"] == nil then
151 decoder = utf8tolatin1ifpossible;
152 elseif response["charset"] ~= "utf-8" then
153 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.";
158 if response["digest-uri"] then
159 protocol, domain = response["digest-uri"]:match("(%w+)/(.*)$");
160 if protocol == nil or domain == nil then return "failure", "malformed-request" end
162 return "failure", "malformed-request", "Missing entry for digest-uri in SASL message."
165 --TODO maybe realm support
166 self.username = response["username"];
168 if self.profile.plain then
169 local password, state = self.profile.plain(response["username"], self.realm)
170 if state == nil then return "failure", "not-authorized"
171 elseif state == false then return "failure", "account-disabled" end
172 Y = md5(response["username"]..":"..response["realm"]..":"..password);
173 elseif self.profile["digest-md5"] then
174 Y, state = self.profile["digest-md5"](response["username"], self.realm, response["realm"], response["charset"])
175 if state == nil then return "failure", "not-authorized"
176 elseif state == false then return "failure", "account-disabled" end
177 elseif self.profile["digest-md5-test"] then
180 --local password_encoding, Y = self.credentials_handler("DIGEST-MD5", response["username"], self.realm, response["realm"], decoder);
181 --if Y == nil then return "failure", "not-authorized"
182 --elseif Y == false then return "failure", "account-disabled" end
184 if response.authzid then
185 if response.authzid == self.username or response.authzid == self.username.."@"..self.realm then
187 log("warn", "Client is violating RFC 3920 (section 6.1, point 7).");
188 A1 = Y..":"..response["nonce"]..":"..response["cnonce"]..":"..response.authzid;
190 return "failure", "invalid-authzid";
193 A1 = Y..":"..response["nonce"]..":"..response["cnonce"];
195 local A2 = "AUTHENTICATE:"..protocol.."/"..domain;
197 local HA1 = md5(A1, true);
198 local HA2 = md5(A2, true);
200 local KD = HA1..":"..response["nonce"]..":"..response["nc"]..":"..response["cnonce"]..":"..response["qop"]..":"..HA2;
201 local response_value = md5(KD, true);
203 if response_value == response["response"] then
205 A2 = ":"..protocol.."/"..domain;
210 KD = HA1..":"..response["nonce"]..":"..response["nc"]..":"..response["cnonce"]..":"..response["qop"]..":"..HA2
211 local rspauth = md5(KD, true);
212 self.authenticated = true;
213 --TODO: considering sending the rspauth in a success node for saving one roundtrip; allowed according to http://tools.ietf.org/html/draft-saintandre-rfc3920bis-09#section-7.3.6
214 return "challenge", serialize({rspauth = rspauth});
216 return "failure", "not-authorized", "The response provided by the client doesn't match the one we calculated."
218 elseif self.step == 3 then
219 if self.authenticated ~= nil then return "success"
220 else return "failure", "malformed-request" end
224 function init(registerMechanism)
225 registerMechanism("DIGEST-MD5", {"plain"}, digest);