X-Git-Url: https://git.enpas.org/?a=blobdiff_plain;f=util%2Fsasl.lua;h=f1d01aedd1b1cddf4bf956eaa2aee725783e0639;hb=f9192eb69a832ec6d61f00a85491932eafca35d6;hp=ef1009c225604d6bda6983db34a666c494c3568c;hpb=cc63313eea27fde85bfaf20cfcde5c96d018b5a5;p=prosody.git diff --git a/util/sasl.lua b/util/sasl.lua index ef1009c2..f1d01aed 100644 --- a/util/sasl.lua +++ b/util/sasl.lua @@ -1,69 +1,53 @@ -local base64 = require "base64" -local md5 = require "md5" -local crypto = require "crypto" +local md5 = require "util.hashes".md5; local log = require "util.logger".init("sasl"); local tostring = tostring; local st = require "util.stanza"; local generate_uuid = require "util.uuid".generate; local s_match = string.match; +local gmatch = string.gmatch +local string = string local math = require "math" local type = type local error = error local print = print +local idna_ascii = require "util.encodings".idna.to_ascii module "sasl" -local function new_plain(onAuth, onSuccess, onFail, onWrite) - local object = { mechanism = "PLAIN", onAuth = onAuth, onSuccess = onSuccess, onFail = onFail, - onWrite = onWrite} - local challenge = base64.encode(""); - --onWrite(st.stanza("challenge", {xmlns = "urn:ietf:params:xml:ns:xmpp-sasl"}):text(challenge)) - object.feed = function(self, stanza) - if stanza.name ~= "response" and stanza.name ~= "auth" then self.onFail("invalid-stanza-tag") end - if stanza.attr.xmlns ~= "urn:ietf:params:xml:ns:xmpp-sasl" then self.onFail("invalid-stanza-namespace") end - local response = base64.decode(stanza[1]) - local authorization = s_match(response, "([^&%z]+)") - local authentication = s_match(response, "%z([^&%z]+)%z") - local password = s_match(response, "%z[^&%z]+%z([^&%z]+)") - if self.onAuth(authentication, password) == true then - self.onWrite(st.stanza("success", {xmlns = "urn:ietf:params:xml:ns:xmpp-sasl"})) - self.onSuccess(authentication) - else - self.onWrite(st.stanza("failure", {xmlns = "urn:ietf:params:xml:ns:xmpp-sasl"}):tag("temporary-auth-failure")); - end - end +local function new_plain(realm, password_handler) + local object = { mechanism = "PLAIN", realm = realm, password_handler = password_handler} + function object.feed(self, message) + + if message == "" or message == nil then return "failure", "malformed-request" end + local response = message + local authorization = s_match(response, "([^&%z]+)") + local authentication = s_match(response, "%z([^&%z]+)%z") + local password = s_match(response, "%z[^&%z]+%z([^&%z]+)") + + if authentication == nil or password == nil then return "failure", "malformed-request" end + + local password_encoding, correct_password = self.password_handler(authentication, self.realm, "PLAIN") + + if correct_password == nil then return "failure", "not-authorized" + elseif correct_password == false then return "failure", "account-disabled" end + + local claimed_password = "" + if password_encoding == nil then claimed_password = password + else claimed_password = password_encoding(password) end + + self.username = authentication + if claimed_password == correct_password then + return "success" + else + return "failure", "not-authorized" + end + end return object end - ---[[ -SERVER: -nonce="3145176401",qop="auth",charset=utf-8,algorithm=md5-sess - -CLIENT: username="tobiasfar",nonce="3145176401",cnonce="pJiW7hzeZLvOSAf7gBzwTzLWe4obYOVDlnNESzQCzGg=",nc=00000001,digest-uri="xmpp/jabber.org",qop=auth,response=99a93ba75235136e6403c3a2ba37089d,charset=utf-8 - -username="tobias",nonce="4406697386",cnonce="wUnT7vYrOB0V8D/lKd5bhpaNCk+hLJwc8T4CBCqp7WM=",nc=00000001,digest-uri="xmpp/luaetta.ath.cx",qop=auth,response=d202b8a1bdf8204816fb23c5f87b6b63,charset=utf-8 - -SERVER: -rspauth=ab66d28c260e97da577ce3aac46a8991 -]]-- -local function new_digest_md5(onAuth, onSuccess, onFail, onWrite) - local function H(s) - return md5.sum(s) - end - - local function KD(k, s) - return H(k..":"..s) - end - - local function HEX(n) - return md5.sumhexa(n) - end - - local function HMAC(k, s) - return crypto.hmac.digest("md5", s, k, true) - end +local function new_digest_md5(realm, password_handler) + --TODO maybe support for authzid local function serialize(message) local data = "" @@ -75,109 +59,115 @@ local function new_digest_md5(onAuth, onSuccess, onFail, onWrite) 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.algorith.."," end + if message["realm"] then data = data..[[realm="]]..message.realm..[[",]] end + if message["rspauth"] then data = data..[[rspauth=]]..message.rspauth.."," end data = data:gsub(",$", "") return data end local function parse(data) message = {} - for k, v in string.gmatch(data, [[([%w%-])="?[%w%-]"?,?]]) do + 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", onAuth = onAuth, onSuccess = onSuccess, onFail = onFail, - onWrite = onWrite } + local object = { mechanism = "DIGEST-MD5", realm = realm, password_handler = password_handler} --TODO: something better than math.random would be nice, maybe OpenSSL's random number generator - object.nonce = math.random(0, 9) - for i = 1, 9 do object.nonce = object.nonce..math.random(0, 9) end - object.step = 1 + object.nonce = generate_uuid() + object.step = 0 object.nonce_count = {} - local challenge = base64.encode(serialize({ nonce = object.nonce, - qop = "auth", - charset = "utf-8", - algorithm = "md5-sess"} )); - object.onWrite(st.stanza("challenge", {xmlns = "urn:ietf:params:xml:ns:xmpp-sasl"}):text(challenge)) - object.feed = function(self, stanza) - print(tostring(stanza)) - if stanza.name ~= "response" and stanza.name ~= "auth" then self.onFail("invalid-stanza-tag") end - if stanza.attr.xmlns ~= "urn:ietf:params:xml:ns:xmpp-sasl" then self.onFail("invalid-stanza-namespace") end - if stanza.name == "auth" then return end - self.step = self.step + 1 - if (self.step == 2) then - - log("debug", tostring(stanza[1])) - local response = parse(base64.decode(stanza[1])) - -- check for replay attack - if response["nonce-count"] then - if self.nonce_count[response["nonce-count"]] then self.onFail("not-authorized") end - end - - -- check for username, it's REQUIRED by RFC 2831 - if not response["username"] then - self.onFail("malformed-request") - end - - -- check for nonce, ... - if not response["nonce"] then - self.onFail("malformed-request") - else - -- check if it's the right nonce - if response["nonce"] ~= self.nonce then self.onFail("malformed-request") end - end - - if not response["cnonce"] then self.onFail("malformed-request") end - if not response["qop"] then response["qop"] = "auth" end - - local hostname = "" - if response["digest-uri"] then - local uri = response["digest-uri"]:gmatch("^(%w)/(%w)") - local protocol = uri[1] - log(protocol) - local hostname = uri[2] - log(hostname) - end - - -- compare response_value with own calculation - local A1-- = H(response["username"]..":"..realm-value, ":", passwd } ), - -- ":", nonce-value, ":", cnonce-value) - local A2 - - local response_value = HEX(KD(HEX(H(A1)), response["nonce"]..":"..response["nonce-count"]..":"..response["cnonce-value"]..":"..response["qop"]..":"..HEX(H(A2)))) - - if response["qop"] == "auth" then - - else - - end - - local response_value = HEX(KD(HEX(H(A1)), response["nonce"]..":"..response["nonce-count"]..":"..response["cnonce-value"]..":"..response["qop"]..":"..HEX(H(A2)))) - - end - --[[ - local authorization = s_match(response, "([^&%z]+)") - local authentication = s_match(response, "%z([^&%z]+)%z") - local password = s_match(response, "%z[^&%z]+%z([^&%z]+)") - if self.onAuth(authentication, password) == true then - self.onWrite(st.stanza("success", {xmlns = "urn:ietf:params:xml:ns:xmpp-sasl"})) - self.onSuccess(authentication) - else - self.onWrite(st.stanza("failure", {xmlns = "urn:ietf:params:xml:ns:xmpp-sasl"}):tag("temporary-auth-failure")); - end]]-- - end + + 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 then response["realm"] = "" 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.password_handler(response["username"], response["realm"], "DIGEST-MD5") + if Y == nil then return "failure", "not-authorized" + elseif Y == false then return "failure", "account-disabled" end + + local A1 = Y..":"..response["nonce"]..":"..response["cnonce"]--:authzid + local A2 = "AUTHENTICATE:"..protocol.."/"..idna_ascii(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.."/"..idna_ascii(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 -function new(mechanism, onAuth, onSuccess, onFail, onWrite) +function new(mechanism, realm, password_handler) local object - if mechanism == "PLAIN" then object = new_plain(onAuth, onSuccess, onFail, onWrite) - elseif mechanism == "DIGEST-MD5" then object = new_digest_md5(onAuth, onSuccess, onFail, onWrite) + if mechanism == "PLAIN" then object = new_plain(realm, password_handler) + elseif mechanism == "DIGEST-MD5" then object = new_digest_md5(realm, password_handler) else log("debug", "Unsupported SASL mechanism: "..tostring(mechanism)); - onFail("unsupported-mechanism") + return nil end return object end