util/sasl.lua

changeset 294
5d861d6e5bbd
parent 292
33175ad2f682
child 297
15b375870b40
equal deleted inserted replaced
293:b446de4e258e 294:5d861d6e5bbd
1 1
2 local base64 = require "base64"
3 local md5 = require "md5" 2 local md5 = require "md5"
4 --local crypto = require "crypto"
5 local log = require "util.logger".init("sasl"); 3 local log = require "util.logger".init("sasl");
6 local tostring = tostring; 4 local tostring = tostring;
7 local st = require "util.stanza"; 5 local st = require "util.stanza";
8 local generate_uuid = require "util.uuid".generate; 6 local generate_uuid = require "util.uuid".generate;
9 local s_match = string.match; 7 local s_match = string.match;
25 local response = message 23 local response = message
26 local authorization = s_match(response, "([^&%z]+)") 24 local authorization = s_match(response, "([^&%z]+)")
27 local authentication = s_match(response, "%z([^&%z]+)%z") 25 local authentication = s_match(response, "%z([^&%z]+)%z")
28 local password = s_match(response, "%z[^&%z]+%z([^&%z]+)") 26 local password = s_match(response, "%z[^&%z]+%z([^&%z]+)")
29 27
30 local password_encoding, correct_password = self.password_handler(authentication.."@"..self.realm, "PLAIN") 28 local password_encoding, correct_password = self.password_handler(authentication, self.realm, "PLAIN")
31 29
32 local claimed_password = "" 30 local claimed_password = ""
33 if password_encoding == nil then claimed_password = password 31 if password_encoding == nil then claimed_password = password
34 else claimed_password = password_encoding(password) end 32 else claimed_password = password_encoding(password) end
35 33
36 self.username = authentication 34 self.username = authentication
37 if claimed_password == correct_password then 35 if claimed_password == correct_password then
38 log("debug", "success") 36 log("debug", "success")
39 return "success", nil 37 return "success"
40 else 38 else
41 log("debug", "failure") 39 log("debug", "failure")
42 return "failure", "not-authorized" 40 return "failure", "not-authorized"
43 end 41 end
44 end 42 end
45 return object 43 return object
46 end 44 end
47 45
48 local function new_digest_md5(onAuth, onSuccess, onFail, onWrite) 46 local function new_digest_md5(realm, password_handler)
49 --TODO maybe support for authzid 47 --TODO maybe support for authzid
50 48
51 local function serialize(message) 49 local function serialize(message)
52 local data = "" 50 local data = ""
53 51
72 log("debug", " "..k.." = "..v) 70 log("debug", " "..k.." = "..v)
73 end 71 end
74 return message 72 return message
75 end 73 end
76 74
77 local object = { mechanism = "DIGEST-MD5", onAuth = onAuth, onSuccess = onSuccess, onFail = onFail, 75 local object = { mechanism = "DIGEST-MD5", realm = realm, password_handler = password_handler}
78 onWrite = onWrite }
79 76
80 --TODO: something better than math.random would be nice, maybe OpenSSL's random number generator 77 --TODO: something better than math.random would be nice, maybe OpenSSL's random number generator
81 object.nonce = generate_uuid() 78 object.nonce = generate_uuid()
82 log("debug", "SASL nonce: "..object.nonce) 79 object.step = 0
83 object.step = 1
84 object.nonce_count = {} 80 object.nonce_count = {}
85 local challenge = base64.encode(serialize({ nonce = object.nonce, 81
86 qop = "auth", 82 function object.feed(self, message)
87 charset = "utf-8", 83 log("debug", "SASL step: "..self.step)
88 algorithm = "md5-sess"} )); 84 self.step = self.step + 1
89 object.onWrite(st.stanza("challenge", {xmlns = "urn:ietf:params:xml:ns:xmpp-sasl"}):text(challenge)) 85 if (self.step == 1) then
90 object.feed = function(self, stanza) 86 local challenge = serialize({ nonce = object.nonce,
91 log("debug", "SASL step: "..self.step) 87 qop = "auth",
92 if stanza.name ~= "response" and stanza.name ~= "auth" then self.onFail("invalid-stanza-tag") end 88 charset = "utf-8",
93 if stanza.attr.xmlns ~= "urn:ietf:params:xml:ns:xmpp-sasl" then self.onFail("invalid-stanza-namespace") end 89 algorithm = "md5-sess",
94 if stanza.name == "auth" then return end 90 realm = self.realm});
95 self.step = self.step + 1 91 log("debug", "challenge: "..challenge)
96 if (self.step == 2) then 92 return "challenge", challenge
97 local response = parse(base64.decode(stanza[1])) 93 elseif (self.step == 2) then
98 -- check for replay attack 94 local response = parse(message)
99 if response["nc"] then 95 -- check for replay attack
100 if self.nonce_count[response["nc"]] then self.onFail("not-authorized") end 96 if response["nc"] then
101 end 97 if self.nonce_count[response["nc"]] then return "failure", "not-authorized" end
102 98 end
103 -- check for username, it's REQUIRED by RFC 2831 99
104 if not response["username"] then 100 -- check for username, it's REQUIRED by RFC 2831
105 self.onFail("malformed-request") 101 if not response["username"] then
106 end 102 return "failure", "malformed-request"
107 self["username"] = response["username"] 103 end
108 104 self["username"] = response["username"]
109 -- check for nonce, ... 105
110 if not response["nonce"] then 106 -- check for nonce, ...
111 self.onFail("malformed-request") 107 if not response["nonce"] then
112 else 108 return "failure", "malformed-request"
113 -- check if it's the right nonce 109 else
114 if response["nonce"] ~= tostring(self.nonce) then self.onFail("malformed-request") end 110 -- check if it's the right nonce
115 end 111 if response["nonce"] ~= tostring(self.nonce) then return "failure", "malformed-request" end
116 112 end
117 if not response["cnonce"] then self.onFail("malformed-request") end 113
118 if not response["qop"] then response["qop"] = "auth" end 114 if not response["cnonce"] then return "failure", "malformed-request" end
119 115 if not response["qop"] then response["qop"] = "auth" end
120 if response["realm"] == nil then response["realm"] = "" end 116
121 117 if response["realm"] == nil then response["realm"] = "" end
122 local domain = "" 118
123 local protocol = "" 119 local domain = ""
124 if response["digest-uri"] then 120 local protocol = ""
125 protocol, domain = response["digest-uri"]:match("(%w+)/(.*)$") 121 if response["digest-uri"] then
126 else 122 protocol, domain = response["digest-uri"]:match("(%w+)/(.*)$")
127 error("No digest-uri") 123 else
128 end 124 return "failure", "malformed-request", "Missing entry for digest-uri in SASL message."
129 125 end
130 -- compare response_value with own calculation 126
131 --local A1 = usermanager.get_md5(response["username"], hostname)..":"..response["nonce"]..response["cnonce"] 127 --TODO maybe realm support
132 128 self.username = response["username"]
133 --FIXME actual username and password here :P 129 local password_encoding, Y = self.password_handler(response["username"], response["realm"], "DIGEST-MD5")
134 local X = "tobias:"..response["realm"]..":tobias" 130 local A1 = Y..":"..response["nonce"]..":"..response["cnonce"]--:authzid
135 local Y = md5.sum(X) 131 local A2 = "AUTHENTICATE:"..protocol.."/"..domain
136 local A1 = Y..":"..response["nonce"]..":"..response["cnonce"]--:authzid 132
137 local A2 = "AUTHENTICATE:"..protocol.."/"..domain 133 local HA1 = md5.sumhexa(A1)
138 134 local HA2 = md5.sumhexa(A2)
139 local HA1 = md5.sumhexa(A1) 135
140 local HA2 = md5.sumhexa(A2) 136 local KD = HA1..":"..response["nonce"]..":"..response["nc"]..":"..response["cnonce"]..":"..response["qop"]..":"..HA2
141 137 local response_value = md5.sumhexa(KD)
142 local KD = HA1..":"..response["nonce"]..":"..response["nc"]..":"..response["cnonce"]..":"..response["qop"]..":"..HA2 138
143 local response_value = md5.sumhexa(KD) 139 log("debug", "response_value: "..response_value);
144 140 log("debug", "response: "..response["response"]);
145 log("debug", "response_value: "..response_value); 141 if response_value == response["response"] then
146 log("debug", "response: "..response["response"]); 142 -- calculate rspauth
147 if response_value == response["response"] then 143 A2 = ":"..protocol.."/"..domain
148 -- calculate rspauth 144
149 A2 = ":"..protocol.."/"..domain 145 HA1 = md5.sumhexa(A1)
150 146 HA2 = md5.sumhexa(A2)
151 HA1 = md5.sumhexa(A1) 147
152 HA2 = md5.sumhexa(A2) 148 KD = HA1..":"..response["nonce"]..":"..response["nc"]..":"..response["cnonce"]..":"..response["qop"]..":"..HA2
153 149 local rspauth = md5.sumhexa(KD)
154 KD = HA1..":"..response["nonce"]..":"..response["nc"]..":"..response["cnonce"]..":"..response["qop"]..":"..HA2 150
155 local rspauth = md5.sumhexa(KD) 151 return "challenge", serialize({rspauth = rspauth})
156 152 else
157 self.onWrite(st.stanza("challenge", {xmlns = "urn:ietf:params:xml:ns:xmpp-sasl"}):text(base64.encode(serialize({rspauth = rspauth})))) 153 return "failure", "not-authorized", "The response provided by the client doesn't match the one we calculated."
158 else 154 end
159 self.onWrite(st.stanza("response", {xmlns = "urn:ietf:params:xml:ns:xmpp-sasl"})) 155 elseif self.step == 3 then
160 self.onFail() 156 return "success"
161 end 157 end
162 elseif self.step == 3 then 158 end
163 if stanza.name == "response" then
164 self.onWrite(st.stanza("success", {xmlns = "urn:ietf:params:xml:ns:xmpp-sasl"}))
165 self.onSuccess(self.username)
166 else
167 self.onFail("Third step isn't a response stanza.")
168 end
169 end
170 end
171 return object 159 return object
172 end 160 end
173 161
174 function new(mechanism, realm, password) 162 function new(mechanism, realm, password_handler)
175 local object 163 local object
176 if mechanism == "PLAIN" then object = new_plain(realm, password) 164 if mechanism == "PLAIN" then object = new_plain(realm, password_handler)
177 --elseif mechanism == "DIGEST-MD5" then object = new_digest_md5(ream, password) 165 elseif mechanism == "DIGEST-MD5" then object = new_digest_md5(realm, password_handler)
178 else 166 else
179 log("debug", "Unsupported SASL mechanism: "..tostring(mechanism)); 167 log("debug", "Unsupported SASL mechanism: "..tostring(mechanism));
180 return nil 168 return nil
181 end 169 end
182 return object 170 return object

mercurial