Software /
code /
prosody
Comparison
util/sasl.lua @ 2175:3ca8755581a1 sasl
Initial commit of the SASL redesign.
author | Tobias Markmann <tm@ayena.de> |
---|---|
date | Mon, 10 Aug 2009 12:14:40 +0200 |
parent | 1585:edc066730d11 |
child | 2176:aaf2b2df61f7 |
comparison
equal
deleted
inserted
replaced
1639:0914d128c55e | 2175:3ca8755581a1 |
---|---|
25 local string = string | 25 local string = string |
26 local math = require "math" | 26 local math = require "math" |
27 local type = type | 27 local type = type |
28 local error = error | 28 local error = error |
29 local print = print | 29 local print = print |
30 local setmetatable = setmetatable; | |
31 local assert = assert; | |
30 | 32 |
31 module "sasl" | 33 module "sasl" |
32 | 34 |
33 -- Credentials handler: | 35 local method = {} |
34 -- Arguments: ("PLAIN", user, host, password) | 36 local mechanisms = {}; |
35 -- Returns: true (success) | false (fail) | nil (user unknown) | 37 local backend_mechanism = {}; |
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 | 38 |
45 if authentication == nil or password == nil then return "failure", "malformed-request" end | 39 -- register a new SASL mechanims |
46 self.username = authentication | 40 local function registerMechanism(name, backends, f) |
47 local auth_success = self.credentials_handler("PLAIN", self.username, self.realm, password) | 41 assert(type(name) == "string", "Parameter name MUST be a string."); |
48 | 42 assert(type(backends) == "string" or type(backends) == "table", "Parameter backends MUST be either a string or a table."); |
49 if auth_success then | 43 assert(type(f) == "function", "Parameter f MUST be a function."); |
50 return "success" | 44 mechanism[name] = f |
51 elseif auth_success == nil then | 45 for _, backend_name in ipairs(backend) |
52 return "failure", "account-disabled" | |
53 else | |
54 return "failure", "not-authorized" | |
55 end | |
56 end | |
57 return object | |
58 end | 46 end |
59 | 47 |
60 -- credentials_handler: | 48 -- create a new SASL object which can be used to authenticate clients |
61 -- Arguments: (mechanism, node, domain, realm, decoder) | 49 function new(realm, profile) |
62 -- Returns: Password encoding, (plaintext) password | 50 sasl_i = {}; |
63 -- implementing RFC 2831 | 51 |
64 local function new_digest_md5(realm, credentials_handler) | 52 return setmetatable(sasl_i, method); |
65 --TODO complete support for authzid | |
66 | |
67 local function serialize(message) | |
68 local data = "" | |
69 | |
70 if type(message) ~= "table" then error("serialize needs an argument of type table.") end | |
71 | |
72 -- testing all possible values | |
73 if message["nonce"] then data = data..[[nonce="]]..message.nonce..[[",]] end | |
74 if message["qop"] then data = data..[[qop="]]..message.qop..[[",]] end | |
75 if message["charset"] then data = data..[[charset=]]..message.charset.."," end | |
76 if message["algorithm"] then data = data..[[algorithm=]]..message.algorithm.."," end | |
77 if message["realm"] then data = data..[[realm="]]..message.realm..[[",]] 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 | |
127 end | |
128 return t_concat(p); | |
129 end | |
130 local function parse(data) | |
131 message = {} | |
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"], to_unicode(domain), 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.."@"..self.realm then | |
207 -- COMPAT | |
208 log("warn", "Client is violating XMPP RFC. See section 6.1 of RFC 3920."); | |
209 A1 = Y..":"..response["nonce"]..":"..response["cnonce"]..":"..response.authzid; | |
210 else | |
211 A1 = "?"; | |
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 | 53 end |
245 | 54 |
246 -- Credentials handler: Can be nil. If specified, should take the mechanism as | 55 -- get a list of possible SASL mechanims to use |
247 -- the only argument, and return true for OK, or false for not-OK (TODO) | 56 function method:mechanisms() |
248 local function new_anonymous(realm, credentials_handler) | 57 |
249 local object = { mechanism = "ANONYMOUS", realm = realm, credentials_handler = credentials_handler} | |
250 function object.feed(self, message) | |
251 return "success" | |
252 end | |
253 object["username"] = generate_uuid() | |
254 return object | |
255 end | 58 end |
256 | 59 |
60 -- select a mechanism to use | |
61 function method.select( mechanism ) | |
257 | 62 |
258 function new(mechanism, realm, credentials_handler) | |
259 local object | |
260 if mechanism == "PLAIN" then object = new_plain(realm, credentials_handler) | |
261 elseif mechanism == "DIGEST-MD5" then object = new_digest_md5(realm, credentials_handler) | |
262 elseif mechanism == "ANONYMOUS" then object = new_anonymous(realm, credentials_handler) | |
263 else | |
264 log("debug", "Unsupported SASL mechanism: "..tostring(mechanism)); | |
265 return nil | |
266 end | |
267 return object | |
268 end | 63 end |
269 | 64 |
270 return _M; | 65 return _M; |