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 |