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.

mwild1@896 1 -- sasl.lua v0.4
mwild1@760 2 -- Copyright (C) 2008-2009 Tobias Markmann
mwild1@519 3 --
mwild1@519 4 -- All rights reserved.
mwild1@519 5 --
mwild1@519 6 -- Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
mwild1@519 7 --
mwild1@519 8 -- * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
mwild1@519 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.
mwild1@519 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.
mwild1@519 11 --
mwild1@519 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.
mwild1@519 13
tm@15 14
waqas20@449 15 local md5 = require "util.hashes".md5;
mwild1@38 16 local log = require "util.logger".init("sasl");
mwild1@38 17 local tostring = tostring;
mwild1@38 18 local st = require "util.stanza";
tm@276 19 local generate_uuid = require "util.uuid".generate;
waqas20@504 20 local t_insert, t_concat = table.insert, table.concat;
waqas20@504 21 local to_byte, to_char = string.byte, string.char;
mwild1@38 22 local s_match = string.match;
tm@277 23 local gmatch = string.gmatch
tm@280 24 local string = string
tm@276 25 local math = require "math"
tm@276 26 local type = type
tm@276 27 local error = error
tm@276 28 local print = print
tm@276 29
mwild1@38 30 module "sasl"
mwild1@38 31
tm@285 32 local function new_plain(realm, password_handler)
tm@285 33 local object = { mechanism = "PLAIN", realm = realm, password_handler = password_handler}
tm@297 34 function object.feed(self, message)
tm@297 35
tm@297 36 if message == "" or message == nil then return "failure", "malformed-request" end
tm@297 37 local response = message
tm@297 38 local authorization = s_match(response, "([^&%z]+)")
tm@297 39 local authentication = s_match(response, "%z([^&%z]+)%z")
tm@297 40 local password = s_match(response, "%z[^&%z]+%z([^&%z]+)")
tm@297 41
tm@402 42 if authentication == nil or password == nil then return "failure", "malformed-request" end
tm@402 43
tm@297 44 local password_encoding, correct_password = self.password_handler(authentication, self.realm, "PLAIN")
tm@297 45
tm@405 46 if correct_password == nil then return "failure", "not-authorized"
tm@404 47 elseif correct_password == false then return "failure", "account-disabled" end
tm@402 48
tm@297 49 local claimed_password = ""
tm@297 50 if password_encoding == nil then claimed_password = password
tm@297 51 else claimed_password = password_encoding(password) end
tm@297 52
tm@297 53 self.username = authentication
tm@297 54 if claimed_password == correct_password then
tm@297 55 return "success"
tm@297 56 else
tm@297 57 return "failure", "not-authorized"
tm@297 58 end
tm@297 59 end
tm@15 60 return object
tm@15 61 end
tm@15 62
tm@1158 63
tm@1158 64 -- implementing RFC 2831
tm@294 65 local function new_digest_md5(realm, password_handler)
tm@1158 66 --TODO complete support for authzid
tm@276 67
tm@276 68 local function serialize(message)
tm@276 69 local data = ""
tm@276 70
tm@276 71 if type(message) ~= "table" then error("serialize needs an argument of type table.") end
tm@276 72
tm@276 73 -- testing all possible values
tm@276 74 if message["nonce"] then data = data..[[nonce="]]..message.nonce..[[",]] end
tm@276 75 if message["qop"] then data = data..[[qop="]]..message.qop..[[",]] end
tm@276 76 if message["charset"] then data = data..[[charset=]]..message.charset.."," end
tm@276 77 if message["algorithm"] then data = data..[[algorithm=]]..message.algorithm.."," end
tm@280 78 if message["realm"] then data = data..[[realm="]]..message.realm..[[",]] end
tm@280 79 if message["rspauth"] then data = data..[[rspauth=]]..message.rspauth.."," end
tm@276 80 data = data:gsub(",$", "")
tm@276 81 return data
tm@276 82 end
tm@276 83
waqas20@595 84 local function utf8tolatin1ifpossible(passwd)
waqas20@595 85 local i = 1;
waqas20@595 86 while i <= #passwd do
waqas20@595 87 local passwd_i = to_byte(passwd:sub(i, i));
waqas20@595 88 if passwd_i > 0x7F then
waqas20@595 89 if passwd_i < 0xC0 or passwd_i > 0xC3 then
waqas20@595 90 return passwd;
waqas20@595 91 end
waqas20@595 92 i = i + 1;
waqas20@595 93 passwd_i = to_byte(passwd:sub(i, i));
waqas20@595 94 if passwd_i < 0x80 or passwd_i > 0xBF then
waqas20@595 95 return passwd;
waqas20@595 96 end
waqas20@595 97 end
waqas20@595 98 i = i + 1;
waqas20@595 99 end
waqas20@595 100
waqas20@595 101 local p = {};
waqas20@595 102 local j = 0;
waqas20@595 103 i = 1;
waqas20@595 104 while (i <= #passwd) do
waqas20@595 105 local passwd_i = to_byte(passwd:sub(i, i));
waqas20@595 106 if passwd_i > 0x7F then
waqas20@595 107 i = i + 1;
waqas20@595 108 local passwd_i_1 = to_byte(passwd:sub(i, i));
waqas20@595 109 t_insert(p, to_char(passwd_i%4*64 + passwd_i_1%64)); -- I'm so clever
waqas20@595 110 else
waqas20@595 111 t_insert(p, to_char(passwd_i));
waqas20@595 112 end
waqas20@595 113 i = i + 1;
waqas20@595 114 end
waqas20@595 115 return t_concat(p);
waqas20@595 116 end
waqas20@504 117 local function latin1toutf8(str)
waqas20@504 118 local p = {};
waqas20@504 119 for ch in gmatch(str, ".") do
waqas20@504 120 ch = to_byte(ch);
waqas20@504 121 if (ch < 0x80) then
waqas20@504 122 t_insert(p, to_char(ch));
waqas20@504 123 elseif (ch < 0xC0) then
waqas20@504 124 t_insert(p, to_char(0xC2, ch));
waqas20@504 125 else
waqas20@504 126 t_insert(p, to_char(0xC3, ch - 64));
waqas20@504 127 end
waqas20@504 128 end
waqas20@504 129 return t_concat(p);
waqas20@504 130 end
tm@276 131 local function parse(data)
tm@276 132 message = {}
waqas20@458 133 for k, v in gmatch(data, [[([%w%-]+)="?([^",]*)"?,?]]) do -- FIXME The hacky regex makes me shudder
tm@1160 134 message[k] = v;
tm@276 135 end
tm@1160 136 return message;
tm@276 137 end
tm@276 138
tm@1160 139 local object = { mechanism = "DIGEST-MD5", realm = realm, password_handler = password_handler};
tm@276 140
tm@1160 141 object.nonce = generate_uuid();
tm@1160 142 object.step = 0;
tm@1160 143 object.nonce_count = {};
tm@294 144
tm@294 145 function object.feed(self, message)
tm@1160 146 self.step = self.step + 1;
tm@294 147 if (self.step == 1) then
tm@294 148 local challenge = serialize({ nonce = object.nonce,
tm@294 149 qop = "auth",
tm@294 150 charset = "utf-8",
tm@294 151 algorithm = "md5-sess",
tm@505 152 realm = self.realm});
tm@1160 153 return "challenge", challenge;
tm@294 154 elseif (self.step == 2) then
tm@1160 155 local response = parse(message);
tm@294 156 -- check for replay attack
tm@294 157 if response["nc"] then
tm@294 158 if self.nonce_count[response["nc"]] then return "failure", "not-authorized" end
tm@294 159 end
tm@294 160
tm@294 161 -- check for username, it's REQUIRED by RFC 2831
tm@294 162 if not response["username"] then
tm@1160 163 return "failure", "malformed-request";
tm@294 164 end
tm@1160 165 self["username"] = response["username"];
tm@294 166
tm@294 167 -- check for nonce, ...
tm@294 168 if not response["nonce"] then
tm@1160 169 return "failure", "malformed-request";
tm@294 170 else
tm@294 171 -- check if it's the right nonce
tm@294 172 if response["nonce"] ~= tostring(self.nonce) then return "failure", "malformed-request" end
tm@294 173 end
tm@294 174
tm@297 175 if not response["cnonce"] then return "failure", "malformed-request", "Missing entry for cnonce in SASL message." end
tm@294 176 if not response["qop"] then response["qop"] = "auth" end
tm@294 177
waqas20@702 178 if response["realm"] == nil or response["realm"] == "" then
waqas20@702 179 response["realm"] = self.realm;
waqas20@702 180 elseif response["realm"] ~= self.realm then
waqas20@602 181 return "failure", "not-authorized", "Incorrect realm value";
waqas20@602 182 end
waqas20@685 183
waqas20@599 184 local decoder;
tm@508 185 if response["charset"] == nil then
waqas20@599 186 decoder = utf8tolatin1ifpossible;
tm@508 187 elseif response["charset"] ~= "utf-8" then
tm@1160 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.";
tm@508 189 end
tm@508 190
tm@1160 191 local domain = "";
tm@1160 192 local protocol = "";
tm@294 193 if response["digest-uri"] then
tm@1160 194 protocol, domain = response["digest-uri"]:match("(%w+)/(.*)$");
tm@402 195 if protocol == nil or domain == nil then return "failure", "malformed-request" end
tm@294 196 else
tm@294 197 return "failure", "malformed-request", "Missing entry for digest-uri in SASL message."
tm@294 198 end
tm@294 199
tm@294 200 --TODO maybe realm support
tm@1160 201 self.username = response["username"];
waqas20@599 202 local password_encoding, Y = self.password_handler(response["username"], response["realm"], "DIGEST-MD5", decoder)
tm@405 203 if Y == nil then return "failure", "not-authorized"
tm@404 204 elseif Y == false then return "failure", "account-disabled" end
tm@1159 205 local A1 = "";
tm@1159 206 if response.authzid then
tm@1159 207 if response.authzid == self.username.."@"..self.realm then
tm@1374 208 -- COMPAT
tm@1161 209 log("warn", "Client is violating XMPP RFC. See section 6.1 of RFC 3920.");
tm@1159 210 A1 = Y..":"..response["nonce"]..":"..response["cnonce"]..":"..response.authzid;
tm@1159 211 else
tm@1159 212 A1 = "?";
tm@1159 213 end
tm@1159 214 else
tm@1159 215 A1 = Y..":"..response["nonce"]..":"..response["cnonce"];
tm@1159 216 end
waqas20@603 217 local A2 = "AUTHENTICATE:"..protocol.."/"..domain;
tm@294 218
tm@1160 219 local HA1 = md5(A1, true);
tm@1160 220 local HA2 = md5(A2, true);
tm@294 221
tm@1160 222 local KD = HA1..":"..response["nonce"]..":"..response["nc"]..":"..response["cnonce"]..":"..response["qop"]..":"..HA2;
tm@1160 223 local response_value = md5(KD, true);
tm@294 224
tm@294 225 if response_value == response["response"] then
tm@294 226 -- calculate rspauth
waqas20@603 227 A2 = ":"..protocol.."/"..domain;
tm@294 228
tm@1160 229 HA1 = md5(A1, true);
tm@1160 230 HA2 = md5(A2, true);
tm@294 231
tm@294 232 KD = HA1..":"..response["nonce"]..":"..response["nc"]..":"..response["cnonce"]..":"..response["qop"]..":"..HA2
tm@1160 233 local rspauth = md5(KD, true);
tm@1160 234 self.authenticated = true;
tm@1160 235 return "challenge", serialize({rspauth = rspauth});
tm@294 236 else
tm@294 237 return "failure", "not-authorized", "The response provided by the client doesn't match the one we calculated."
tm@294 238 end
tm@294 239 elseif self.step == 3 then
tm@297 240 if self.authenticated ~= nil then return "success"
tm@297 241 else return "failure", "malformed-request" end
tm@294 242 end
tm@294 243 end
tm@1160 244 return object;
tm@276 245 end
tm@276 246
tm@799 247 local function new_anonymous(realm, password_handler)
tm@799 248 local object = { mechanism = "ANONYMOUS", realm = realm, password_handler = password_handler}
tm@799 249 function object.feed(self, message)
tm@799 250 return "success"
tm@799 251 end
tm@799 252 object["username"] = generate_uuid()
tm@799 253 return object
tm@799 254 end
tm@799 255
tm@799 256
tm@294 257 function new(mechanism, realm, password_handler)
tm@15 258 local object
tm@294 259 if mechanism == "PLAIN" then object = new_plain(realm, password_handler)
tm@294 260 elseif mechanism == "DIGEST-MD5" then object = new_digest_md5(realm, password_handler)
tm@799 261 elseif mechanism == "ANONYMOUS" then object = new_anonymous(realm, password_handler)
mwild1@38 262 else
mwild1@38 263 log("debug", "Unsupported SASL mechanism: "..tostring(mechanism));
tm@285 264 return nil
tm@15 265 end
tm@15 266 return object
tm@15 267 end
tm@15 268
mwild1@519 269 return _M;

mercurial