Software /
code /
prosody
Comparison
util/sasl.lua @ 2193:8fbbdb11a520
Merge with sasl branch.
author | Tobias Markmann <tm@ayena.de> |
---|---|
date | Mon, 16 Nov 2009 21:43:57 +0100 |
parent | 2080:ca419b92a8c7 |
parent | 2191:e79c0ce6cf54 |
child | 2198:d18b4d22b8da |
comparison
equal
deleted
inserted
replaced
2080:ca419b92a8c7 | 2193:8fbbdb11a520 |
---|---|
14 | 14 |
15 local md5 = require "util.hashes".md5; | 15 local md5 = require "util.hashes".md5; |
16 local log = require "util.logger".init("sasl"); | 16 local log = require "util.logger".init("sasl"); |
17 local tostring = tostring; | 17 local tostring = tostring; |
18 local st = require "util.stanza"; | 18 local st = require "util.stanza"; |
19 local generate_uuid = require "util.uuid".generate; | 19 local pairs, ipairs = pairs, ipairs; |
20 local t_insert, t_concat = table.insert, table.concat; | 20 local t_insert, t_concat = table.insert, table.concat; |
21 local to_byte, to_char = string.byte, string.char; | |
22 local to_unicode = require "util.encodings".idna.to_unicode; | 21 local to_unicode = require "util.encodings".idna.to_unicode; |
23 local s_match = string.match; | 22 local s_match = string.match; |
24 local gmatch = string.gmatch | 23 local gmatch = string.gmatch |
25 local string = string | 24 local string = string |
26 local math = require "math" | 25 local math = require "math" |
27 local type = type | 26 local type = type |
28 local error = error | 27 local error = error |
29 local print = print | 28 local print = print |
29 local setmetatable = setmetatable; | |
30 local assert = assert; | |
31 local dofile = dofile; | |
32 local require = require; | |
30 | 33 |
34 require "util.iterators" | |
35 local keys = keys | |
36 | |
37 local array = require "util.array" | |
31 module "sasl" | 38 module "sasl" |
32 | 39 |
33 -- Credentials handler: | 40 --[[ |
34 -- Arguments: ("PLAIN", user, host, password) | 41 Authentication Backend Prototypes: |
35 -- Returns: true (success) | false (fail) | nil (user unknown) | |
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 | 42 |
45 if authentication == nil or password == nil then return "failure", "malformed-request" end | 43 state = false : disabled |
46 self.username = authentication | 44 state = true : enabled |
47 local auth_success = self.credentials_handler("PLAIN", self.username, self.realm, password) | 45 state = nil : non-existant |
48 | 46 |
49 if auth_success then | 47 plain: |
50 return "success" | 48 function(username, realm) |
51 elseif auth_success == nil then | 49 return password, state; |
52 return "failure", "account-disabled" | 50 end |
53 else | 51 |
54 return "failure", "not-authorized" | 52 plain-test: |
55 end | 53 function(username, realm, password) |
56 end | 54 return true or false, state; |
57 return object | 55 end |
56 | |
57 digest-md5: | |
58 function(username, domain, realm, encoding) -- domain and realm are usually the same; for some broken | |
59 -- implementations it's not | |
60 return digesthash, state; | |
61 end | |
62 | |
63 digest-md5-test: | |
64 function(username, domain, realm, encoding, digesthash) | |
65 return true or false, state; | |
66 end | |
67 ]] | |
68 | |
69 local method = {}; | |
70 method.__index = method; | |
71 local mechanisms = {}; | |
72 local backend_mechanism = {}; | |
73 | |
74 -- register a new SASL mechanims | |
75 local function registerMechanism(name, backends, f) | |
76 assert(type(name) == "string", "Parameter name MUST be a string."); | |
77 assert(type(backends) == "string" or type(backends) == "table", "Parameter backends MUST be either a string or a table."); | |
78 assert(type(f) == "function", "Parameter f MUST be a function."); | |
79 mechanisms[name] = f | |
80 for _, backend_name in ipairs(backends) do | |
81 if backend_mechanism[backend_name] == nil then backend_mechanism[backend_name] = {}; end | |
82 t_insert(backend_mechanism[backend_name], name); | |
83 end | |
58 end | 84 end |
59 | 85 |
60 -- credentials_handler: | 86 -- create a new SASL object which can be used to authenticate clients |
61 -- Arguments: (mechanism, node, domain, realm, decoder) | 87 function new(realm, profile) |
62 -- Returns: Password encoding, (plaintext) password | 88 sasl_i = {profile = profile}; |
63 -- implementing RFC 2831 | 89 sasl_i.realm = realm; |
64 local function new_digest_md5(realm, credentials_handler) | 90 return setmetatable(sasl_i, method); |
65 --TODO complete support for authzid | 91 end |
66 | 92 |
67 local function serialize(message) | 93 -- get a list of possible SASL mechanims to use |
68 local data = "" | 94 function method:mechanisms() |
69 | 95 local mechanisms = {} |
70 if type(message) ~= "table" then error("serialize needs an argument of type table.") end | 96 for backend, f in pairs(self.profile) do |
71 | 97 print(backend) |
72 -- testing all possible values | 98 if backend_mechanism[backend] then |
73 if message["realm"] then data = data..[[realm="]]..message.realm..[[",]] end | 99 for _, mechanism in ipairs(backend_mechanism[backend]) do |
74 if message["nonce"] then data = data..[[nonce="]]..message.nonce..[[",]] end | 100 mechanisms[mechanism] = true; |
75 if message["qop"] then data = data..[[qop="]]..message.qop..[[",]] end | |
76 if message["charset"] then data = data..[[charset=]]..message.charset.."," end | |
77 if message["algorithm"] then data = data..[[algorithm=]]..message.algorithm.."," 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 | 101 end |
127 end | 102 end |
128 return t_concat(p); | |
129 end | 103 end |
130 local function parse(data) | 104 self["possible_mechanisms"] = mechanisms; |
131 local message = {} | 105 return array.collect(keys(mechanisms)); |
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"], self.realm, 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 or response.authzid == self.username.."@"..self.realm then | |
207 -- COMPAT | |
208 log("warn", "Client is violating RFC 3920 (section 6.1, point 7)."); | |
209 A1 = Y..":"..response["nonce"]..":"..response["cnonce"]..":"..response.authzid; | |
210 else | |
211 return "failure", "invalid-authzid"; | |
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 | 106 end |
245 | 107 |
246 -- Credentials handler: Can be nil. If specified, should take the mechanism as | 108 -- select a mechanism to use |
247 -- the only argument, and return true for OK, or false for not-OK (TODO) | 109 function method:select(mechanism) |
248 local function new_anonymous(realm, credentials_handler) | 110 if self.mech_i then |
249 local object = { mechanism = "ANONYMOUS", realm = realm, credentials_handler = credentials_handler} | 111 return false; |
250 function object.feed(self, message) | 112 end |
251 return "success" | 113 |
252 end | 114 self.mech_i = mechanisms[mechanism] |
253 object["username"] = generate_uuid() | 115 if self.mech_i == nil then |
254 return object | 116 return false; |
117 end | |
118 return true; | |
255 end | 119 end |
256 | 120 |
121 -- feed new messages to process into the library | |
122 function method:process(message) | |
123 --if message == "" or message == nil then return "failure", "malformed-request" end | |
124 return self.mech_i(self, message); | |
125 end | |
257 | 126 |
258 function new(mechanism, realm, credentials_handler) | 127 -- load the mechanisms |
259 local object | 128 load_mechs = {"plain", "digest-md5", "anonymous"} |
260 if mechanism == "PLAIN" then object = new_plain(realm, credentials_handler) | 129 for _, mech in ipairs(load_mechs) do |
261 elseif mechanism == "DIGEST-MD5" then object = new_digest_md5(realm, credentials_handler) | 130 local name = "util.sasl."..mech; |
262 elseif mechanism == "ANONYMOUS" then object = new_anonymous(realm, credentials_handler) | 131 local m = require(name); |
263 else | 132 m.init(registerMechanism) |
264 log("debug", "Unsupported SASL mechanism: "..tostring(mechanism)); | |
265 return nil | |
266 end | |
267 return object | |
268 end | 133 end |
269 | 134 |
270 return _M; | 135 return _M; |