Software /
code /
prosody
Comparison
util/sasl.lua @ 294:5d861d6e5bbd
Made SASL module fit the new interface.
author | Tobias Markmann <tm@ayena.de> |
---|---|
date | Sat, 15 Nov 2008 22:30:09 +0100 |
parent | 292:33175ad2f682 |
child | 297:15b375870b40 |
comparison
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 |