Mercurial > prosody-hg
comparison util/sasl.lua @ 2193:8fbbdb11a520
Merge with sasl branch.
| author | Tobias Markmann <tm@ayena.de> |
|---|---|
| date | Mon, 16 Nov 2009 21:43:57 +0100 |
| parents | ca419b92a8c7 e79c0ce6cf54 |
| children | d18b4d22b8da |
comparison
equal
deleted
inserted
replaced
| 2080:ca419b92a8c7 | 2193:8fbbdb11a520 |
|---|---|
| 14 | 14 |
| 15 local md5 = require "util.hashes".md5; | 15 local md5 = require "util.hashes".md5; |
| 16 local log = require "util.logger".init("sasl"); | 16 local log = require "util.logger".init("sasl"); |
| 17 local tostring = tostring; | 17 local tostring = tostring; |
| 18 local st = require "util.stanza"; | 18 local st = require "util.stanza"; |
| 19 local generate_uuid = require "util.uuid".generate; | 19 local pairs, ipairs = pairs, ipairs; |
| 20 local t_insert, t_concat = table.insert, table.concat; | 20 local t_insert, t_concat = table.insert, table.concat; |
| 21 local to_byte, to_char = string.byte, string.char; | |
| 22 local to_unicode = require "util.encodings".idna.to_unicode; | 21 local to_unicode = require "util.encodings".idna.to_unicode; |
| 23 local s_match = string.match; | 22 local s_match = string.match; |
| 24 local gmatch = string.gmatch | 23 local gmatch = string.gmatch |
| 25 local string = string | 24 local string = string |
| 26 local math = require "math" | 25 local math = require "math" |
| 27 local type = type | 26 local type = type |
| 28 local error = error | 27 local error = error |
| 29 local print = print | 28 local print = print |
| 29 local setmetatable = setmetatable; | |
| 30 local assert = assert; | |
| 31 local dofile = dofile; | |
| 32 local require = require; | |
| 30 | 33 |
| 34 require "util.iterators" | |
| 35 local keys = keys | |
| 36 | |
| 37 local array = require "util.array" | |
| 31 module "sasl" | 38 module "sasl" |
| 32 | 39 |
| 33 -- Credentials handler: | 40 --[[ |
| 34 -- Arguments: ("PLAIN", user, host, password) | 41 Authentication Backend Prototypes: |
| 35 -- Returns: true (success) | false (fail) | nil (user unknown) | |
| 36 local function new_plain(realm, credentials_handler) | |
| 37 local object = { mechanism = "PLAIN", realm = realm, credentials_handler = credentials_handler} | |
| 38 function object.feed(self, message) | |
| 39 if message == "" or message == nil then return "failure", "malformed-request" end | |
| 40 local response = message | |
| 41 local authorization = s_match(response, "([^%z]+)") | |
| 42 local authentication = s_match(response, "%z([^%z]+)%z") | |
| 43 local password = s_match(response, "%z[^%z]+%z([^%z]+)") | |
| 44 | 42 |
| 45 if authentication == nil or password == nil then return "failure", "malformed-request" end | 43 state = false : disabled |
| 46 self.username = authentication | 44 state = true : enabled |
| 47 local auth_success = self.credentials_handler("PLAIN", self.username, self.realm, password) | 45 state = nil : non-existant |
| 48 | 46 |
| 49 if auth_success then | 47 plain: |
| 50 return "success" | 48 function(username, realm) |
| 51 elseif auth_success == nil then | 49 return password, state; |
| 52 return "failure", "account-disabled" | 50 end |
| 53 else | 51 |
| 54 return "failure", "not-authorized" | 52 plain-test: |
| 55 end | 53 function(username, realm, password) |
| 56 end | 54 return true or false, state; |
| 57 return object | 55 end |
| 56 | |
| 57 digest-md5: | |
| 58 function(username, domain, realm, encoding) -- domain and realm are usually the same; for some broken | |
| 59 -- implementations it's not | |
| 60 return digesthash, state; | |
| 61 end | |
| 62 | |
| 63 digest-md5-test: | |
| 64 function(username, domain, realm, encoding, digesthash) | |
| 65 return true or false, state; | |
| 66 end | |
| 67 ]] | |
| 68 | |
| 69 local method = {}; | |
| 70 method.__index = method; | |
| 71 local mechanisms = {}; | |
| 72 local backend_mechanism = {}; | |
| 73 | |
| 74 -- register a new SASL mechanims | |
| 75 local function registerMechanism(name, backends, f) | |
| 76 assert(type(name) == "string", "Parameter name MUST be a string."); | |
| 77 assert(type(backends) == "string" or type(backends) == "table", "Parameter backends MUST be either a string or a table."); | |
| 78 assert(type(f) == "function", "Parameter f MUST be a function."); | |
| 79 mechanisms[name] = f | |
| 80 for _, backend_name in ipairs(backends) do | |
| 81 if backend_mechanism[backend_name] == nil then backend_mechanism[backend_name] = {}; end | |
| 82 t_insert(backend_mechanism[backend_name], name); | |
| 83 end | |
| 58 end | 84 end |
| 59 | 85 |
| 60 -- credentials_handler: | 86 -- create a new SASL object which can be used to authenticate clients |
| 61 -- Arguments: (mechanism, node, domain, realm, decoder) | 87 function new(realm, profile) |
| 62 -- Returns: Password encoding, (plaintext) password | 88 sasl_i = {profile = profile}; |
| 63 -- implementing RFC 2831 | 89 sasl_i.realm = realm; |
| 64 local function new_digest_md5(realm, credentials_handler) | 90 return setmetatable(sasl_i, method); |
| 65 --TODO complete support for authzid | 91 end |
| 66 | 92 |
| 67 local function serialize(message) | 93 -- get a list of possible SASL mechanims to use |
| 68 local data = "" | 94 function method:mechanisms() |
| 69 | 95 local mechanisms = {} |
| 70 if type(message) ~= "table" then error("serialize needs an argument of type table.") end | 96 for backend, f in pairs(self.profile) do |
| 71 | 97 print(backend) |
| 72 -- testing all possible values | 98 if backend_mechanism[backend] then |
| 73 if message["realm"] then data = data..[[realm="]]..message.realm..[[",]] end | 99 for _, mechanism in ipairs(backend_mechanism[backend]) do |
| 74 if message["nonce"] then data = data..[[nonce="]]..message.nonce..[[",]] end | 100 mechanisms[mechanism] = true; |
| 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["rspauth"] then data = data..[[rspauth=]]..message.rspauth.."," end | |
| 79 data = data:gsub(",$", "") | |
| 80 return data | |
| 81 end | |
| 82 | |
| 83 local function utf8tolatin1ifpossible(passwd) | |
| 84 local i = 1; | |
| 85 while i <= #passwd do | |
| 86 local passwd_i = to_byte(passwd:sub(i, i)); | |
| 87 if passwd_i > 0x7F then | |
| 88 if passwd_i < 0xC0 or passwd_i > 0xC3 then | |
| 89 return passwd; | |
| 90 end | |
| 91 i = i + 1; | |
| 92 passwd_i = to_byte(passwd:sub(i, i)); | |
| 93 if passwd_i < 0x80 or passwd_i > 0xBF then | |
| 94 return passwd; | |
| 95 end | |
| 96 end | |
| 97 i = i + 1; | |
| 98 end | |
| 99 | |
| 100 local p = {}; | |
| 101 local j = 0; | |
| 102 i = 1; | |
| 103 while (i <= #passwd) do | |
| 104 local passwd_i = to_byte(passwd:sub(i, i)); | |
| 105 if passwd_i > 0x7F then | |
| 106 i = i + 1; | |
| 107 local passwd_i_1 = to_byte(passwd:sub(i, i)); | |
| 108 t_insert(p, to_char(passwd_i%4*64 + passwd_i_1%64)); -- I'm so clever | |
| 109 else | |
| 110 t_insert(p, to_char(passwd_i)); | |
| 111 end | |
| 112 i = i + 1; | |
| 113 end | |
| 114 return t_concat(p); | |
| 115 end | |
| 116 local function latin1toutf8(str) | |
| 117 local p = {}; | |
| 118 for ch in gmatch(str, ".") do | |
| 119 ch = to_byte(ch); | |
| 120 if (ch < 0x80) then | |
| 121 t_insert(p, to_char(ch)); | |
| 122 elseif (ch < 0xC0) then | |
| 123 t_insert(p, to_char(0xC2, ch)); | |
| 124 else | |
| 125 t_insert(p, to_char(0xC3, ch - 64)); | |
| 126 end | 101 end |
| 127 end | 102 end |
| 128 return t_concat(p); | |
| 129 end | 103 end |
| 130 local function parse(data) | 104 self["possible_mechanisms"] = mechanisms; |
| 131 local message = {} | 105 return array.collect(keys(mechanisms)); |
| 132 for k, v in gmatch(data, [[([%w%-]+)="?([^",]*)"?,?]]) do -- FIXME The hacky regex makes me shudder | |
| 133 message[k] = v; | |
| 134 end | |
| 135 return message; | |
| 136 end | |
| 137 | |
| 138 local object = { mechanism = "DIGEST-MD5", realm = realm, credentials_handler = credentials_handler}; | |
| 139 | |
| 140 object.nonce = generate_uuid(); | |
| 141 object.step = 0; | |
| 142 object.nonce_count = {}; | |
| 143 | |
| 144 function object.feed(self, message) | |
| 145 self.step = self.step + 1; | |
| 146 if (self.step == 1) then | |
| 147 local challenge = serialize({ nonce = object.nonce, | |
| 148 qop = "auth", | |
| 149 charset = "utf-8", | |
| 150 algorithm = "md5-sess", | |
| 151 realm = self.realm}); | |
| 152 return "challenge", challenge; | |
| 153 elseif (self.step == 2) then | |
| 154 local response = parse(message); | |
| 155 -- check for replay attack | |
| 156 if response["nc"] then | |
| 157 if self.nonce_count[response["nc"]] then return "failure", "not-authorized" end | |
| 158 end | |
| 159 | |
| 160 -- check for username, it's REQUIRED by RFC 2831 | |
| 161 if not response["username"] then | |
| 162 return "failure", "malformed-request"; | |
| 163 end | |
| 164 self["username"] = response["username"]; | |
| 165 | |
| 166 -- check for nonce, ... | |
| 167 if not response["nonce"] then | |
| 168 return "failure", "malformed-request"; | |
| 169 else | |
| 170 -- check if it's the right nonce | |
| 171 if response["nonce"] ~= tostring(self.nonce) then return "failure", "malformed-request" end | |
| 172 end | |
| 173 | |
| 174 if not response["cnonce"] then return "failure", "malformed-request", "Missing entry for cnonce in SASL message." end | |
| 175 if not response["qop"] then response["qop"] = "auth" end | |
| 176 | |
| 177 if response["realm"] == nil or response["realm"] == "" then | |
| 178 response["realm"] = ""; | |
| 179 elseif response["realm"] ~= self.realm then | |
| 180 return "failure", "not-authorized", "Incorrect realm value"; | |
| 181 end | |
| 182 | |
| 183 local decoder; | |
| 184 if response["charset"] == nil then | |
| 185 decoder = utf8tolatin1ifpossible; | |
| 186 elseif response["charset"] ~= "utf-8" then | |
| 187 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."; | |
| 188 end | |
| 189 | |
| 190 local domain = ""; | |
| 191 local protocol = ""; | |
| 192 if response["digest-uri"] then | |
| 193 protocol, domain = response["digest-uri"]:match("(%w+)/(.*)$"); | |
| 194 if protocol == nil or domain == nil then return "failure", "malformed-request" end | |
| 195 else | |
| 196 return "failure", "malformed-request", "Missing entry for digest-uri in SASL message." | |
| 197 end | |
| 198 | |
| 199 --TODO maybe realm support | |
| 200 self.username = response["username"]; | |
| 201 local password_encoding, Y = self.credentials_handler("DIGEST-MD5", response["username"], self.realm, response["realm"], decoder); | |
| 202 if Y == nil then return "failure", "not-authorized" | |
| 203 elseif Y == false then return "failure", "account-disabled" end | |
| 204 local A1 = ""; | |
| 205 if response.authzid then | |
| 206 if response.authzid == self.username or response.authzid == self.username.."@"..self.realm then | |
| 207 -- COMPAT | |
| 208 log("warn", "Client is violating RFC 3920 (section 6.1, point 7)."); | |
| 209 A1 = Y..":"..response["nonce"]..":"..response["cnonce"]..":"..response.authzid; | |
| 210 else | |
| 211 return "failure", "invalid-authzid"; | |
| 212 end | |
| 213 else | |
| 214 A1 = Y..":"..response["nonce"]..":"..response["cnonce"]; | |
| 215 end | |
| 216 local A2 = "AUTHENTICATE:"..protocol.."/"..domain; | |
| 217 | |
| 218 local HA1 = md5(A1, true); | |
| 219 local HA2 = md5(A2, true); | |
| 220 | |
| 221 local KD = HA1..":"..response["nonce"]..":"..response["nc"]..":"..response["cnonce"]..":"..response["qop"]..":"..HA2; | |
| 222 local response_value = md5(KD, true); | |
| 223 | |
| 224 if response_value == response["response"] then | |
| 225 -- calculate rspauth | |
| 226 A2 = ":"..protocol.."/"..domain; | |
| 227 | |
| 228 HA1 = md5(A1, true); | |
| 229 HA2 = md5(A2, true); | |
| 230 | |
| 231 KD = HA1..":"..response["nonce"]..":"..response["nc"]..":"..response["cnonce"]..":"..response["qop"]..":"..HA2 | |
| 232 local rspauth = md5(KD, true); | |
| 233 self.authenticated = true; | |
| 234 return "challenge", serialize({rspauth = rspauth}); | |
| 235 else | |
| 236 return "failure", "not-authorized", "The response provided by the client doesn't match the one we calculated." | |
| 237 end | |
| 238 elseif self.step == 3 then | |
| 239 if self.authenticated ~= nil then return "success" | |
| 240 else return "failure", "malformed-request" end | |
| 241 end | |
| 242 end | |
| 243 return object; | |
| 244 end | 106 end |
| 245 | 107 |
| 246 -- Credentials handler: Can be nil. If specified, should take the mechanism as | 108 -- select a mechanism to use |
| 247 -- the only argument, and return true for OK, or false for not-OK (TODO) | 109 function method:select(mechanism) |
| 248 local function new_anonymous(realm, credentials_handler) | 110 if self.mech_i then |
| 249 local object = { mechanism = "ANONYMOUS", realm = realm, credentials_handler = credentials_handler} | 111 return false; |
| 250 function object.feed(self, message) | 112 end |
| 251 return "success" | 113 |
| 252 end | 114 self.mech_i = mechanisms[mechanism] |
| 253 object["username"] = generate_uuid() | 115 if self.mech_i == nil then |
| 254 return object | 116 return false; |
| 117 end | |
| 118 return true; | |
| 255 end | 119 end |
| 256 | 120 |
| 121 -- feed new messages to process into the library | |
| 122 function method:process(message) | |
| 123 --if message == "" or message == nil then return "failure", "malformed-request" end | |
| 124 return self.mech_i(self, message); | |
| 125 end | |
| 257 | 126 |
| 258 function new(mechanism, realm, credentials_handler) | 127 -- load the mechanisms |
| 259 local object | 128 load_mechs = {"plain", "digest-md5", "anonymous"} |
| 260 if mechanism == "PLAIN" then object = new_plain(realm, credentials_handler) | 129 for _, mech in ipairs(load_mechs) do |
| 261 elseif mechanism == "DIGEST-MD5" then object = new_digest_md5(realm, credentials_handler) | 130 local name = "util.sasl."..mech; |
| 262 elseif mechanism == "ANONYMOUS" then object = new_anonymous(realm, credentials_handler) | 131 local m = require(name); |
| 263 else | 132 m.init(registerMechanism) |
| 264 log("debug", "Unsupported SASL mechanism: "..tostring(mechanism)); | |
| 265 return nil | |
| 266 end | |
| 267 return object | |
| 268 end | 133 end |
| 269 | 134 |
| 270 return _M; | 135 return _M; |
