1 |
1 |
2 local base64 = require "base64" |
2 local base64 = require "base64" |
|
3 local md5 = require "md5" |
|
4 local crypto = require "crypto" |
3 local log = require "util.logger".init("sasl"); |
5 local log = require "util.logger".init("sasl"); |
4 local tostring = tostring; |
6 local tostring = tostring; |
5 local st = require "util.stanza"; |
7 local st = require "util.stanza"; |
|
8 local generate_uuid = require "util.uuid".generate; |
6 local s_match = string.match; |
9 local s_match = string.match; |
|
10 local math = require "math" |
|
11 local type = type |
|
12 local error = error |
|
13 local print = print |
|
14 |
7 module "sasl" |
15 module "sasl" |
8 |
|
9 |
16 |
10 local function new_plain(onAuth, onSuccess, onFail, onWrite) |
17 local function new_plain(onAuth, onSuccess, onFail, onWrite) |
11 local object = { mechanism = "PLAIN", onAuth = onAuth, onSuccess = onSuccess, onFail = onFail, |
18 local object = { mechanism = "PLAIN", onAuth = onAuth, onSuccess = onSuccess, onFail = onFail, |
12 onWrite = onWrite} |
19 onWrite = onWrite} |
13 --local challenge = base64.encode(""); |
20 local challenge = base64.encode(""); |
14 --onWrite(st.stanza("challenge", {xmlns = "urn:ietf:params:xml:ns:xmpp-sasl"}):text(challenge)) |
21 --onWrite(st.stanza("challenge", {xmlns = "urn:ietf:params:xml:ns:xmpp-sasl"}):text(challenge)) |
15 object.feed = function(self, stanza) |
22 object.feed = function(self, stanza) |
16 if stanza.name ~= "response" and stanza.name ~= "auth" then self.onFail("invalid-stanza-tag") end |
23 if stanza.name ~= "response" and stanza.name ~= "auth" then self.onFail("invalid-stanza-tag") end |
17 if stanza.attr.xmlns ~= "urn:ietf:params:xml:ns:xmpp-sasl" then self.onFail("invalid-stanza-namespace") end |
24 if stanza.attr.xmlns ~= "urn:ietf:params:xml:ns:xmpp-sasl" then self.onFail("invalid-stanza-namespace") end |
18 local response = base64.decode(stanza[1]) |
25 local response = base64.decode(stanza[1]) |
28 end |
35 end |
29 return object |
36 return object |
30 end |
37 end |
31 |
38 |
32 |
39 |
|
40 --[[ |
|
41 SERVER: |
|
42 nonce="3145176401",qop="auth",charset=utf-8,algorithm=md5-sess |
|
43 |
|
44 CLIENT: username="tobiasfar",nonce="3145176401",cnonce="pJiW7hzeZLvOSAf7gBzwTzLWe4obYOVDlnNESzQCzGg=",nc=00000001,digest-uri="xmpp/jabber.org",qop=auth,response=99a93ba75235136e6403c3a2ba37089d,charset=utf-8 |
|
45 |
|
46 username="tobias",nonce="4406697386",cnonce="wUnT7vYrOB0V8D/lKd5bhpaNCk+hLJwc8T4CBCqp7WM=",nc=00000001,digest-uri="xmpp/luaetta.ath.cx",qop=auth,response=d202b8a1bdf8204816fb23c5f87b6b63,charset=utf-8 |
|
47 |
|
48 SERVER: |
|
49 rspauth=ab66d28c260e97da577ce3aac46a8991 |
|
50 ]]-- |
|
51 local function new_digest_md5(onAuth, onSuccess, onFail, onWrite) |
|
52 local function H(s) |
|
53 return md5.sum(s) |
|
54 end |
|
55 |
|
56 local function KD(k, s) |
|
57 return H(k..":"..s) |
|
58 end |
|
59 |
|
60 local function HEX(n) |
|
61 return md5.sumhexa(n) |
|
62 end |
|
63 |
|
64 local function HMAC(k, s) |
|
65 return crypto.hmac.digest("md5", s, k, true) |
|
66 end |
|
67 |
|
68 local function serialize(message) |
|
69 local data = "" |
|
70 |
|
71 if type(message) ~= "table" then error("serialize needs an argument of type table.") end |
|
72 |
|
73 -- testing all possible values |
|
74 if message["nonce"] then data = data..[[nonce="]]..message.nonce..[[",]] end |
|
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.algorith.."," end |
|
79 data = data:gsub(",$", "") |
|
80 return data |
|
81 end |
|
82 |
|
83 local function parse(data) |
|
84 message = {} |
|
85 for k, v in string.gmatch(data, [[([%w%-])="?[%w%-]"?,?]]) do |
|
86 message[k] = v |
|
87 end |
|
88 return message |
|
89 end |
|
90 |
|
91 local object = { mechanism = "DIGEST-MD5", onAuth = onAuth, onSuccess = onSuccess, onFail = onFail, |
|
92 onWrite = onWrite } |
|
93 |
|
94 --TODO: something better than math.random would be nice, maybe OpenSSL's random number generator |
|
95 object.nonce = math.random(0, 9) |
|
96 for i = 1, 9 do object.nonce = object.nonce..math.random(0, 9) end |
|
97 object.step = 1 |
|
98 object.nonce_count = {} |
|
99 local challenge = base64.encode(serialize({ nonce = object.nonce, |
|
100 qop = "auth", |
|
101 charset = "utf-8", |
|
102 algorithm = "md5-sess"} )); |
|
103 object.onWrite(st.stanza("challenge", {xmlns = "urn:ietf:params:xml:ns:xmpp-sasl"}):text(challenge)) |
|
104 object.feed = function(self, stanza) |
|
105 print(tostring(stanza)) |
|
106 if stanza.name ~= "response" and stanza.name ~= "auth" then self.onFail("invalid-stanza-tag") end |
|
107 if stanza.attr.xmlns ~= "urn:ietf:params:xml:ns:xmpp-sasl" then self.onFail("invalid-stanza-namespace") end |
|
108 if stanza.name == "auth" then return end |
|
109 self.step = self.step + 1 |
|
110 if (self.step == 2) then |
|
111 |
|
112 log("debug", tostring(stanza[1])) |
|
113 local response = parse(base64.decode(stanza[1])) |
|
114 -- check for replay attack |
|
115 if response["nonce-count"] then |
|
116 if self.nonce_count[response["nonce-count"]] then self.onFail("not-authorized") end |
|
117 end |
|
118 |
|
119 -- check for username, it's REQUIRED by RFC 2831 |
|
120 if not response["username"] then |
|
121 self.onFail("malformed-request") |
|
122 end |
|
123 |
|
124 -- check for nonce, ... |
|
125 if not response["nonce"] then |
|
126 self.onFail("malformed-request") |
|
127 else |
|
128 -- check if it's the right nonce |
|
129 if response["nonce"] ~= self.nonce then self.onFail("malformed-request") end |
|
130 end |
|
131 |
|
132 if not response["cnonce"] then self.onFail("malformed-request") end |
|
133 if not response["qop"] then response["qop"] = "auth" end |
|
134 |
|
135 local hostname = "" |
|
136 if response["digest-uri"] then |
|
137 local uri = response["digest-uri"]:gmatch("^(%w)/(%w)") |
|
138 local protocol = uri[1] |
|
139 log(protocol) |
|
140 local hostname = uri[2] |
|
141 log(hostname) |
|
142 end |
|
143 |
|
144 -- compare response_value with own calculation |
|
145 local A1-- = H(response["username"]..":"..realm-value, ":", passwd } ), |
|
146 -- ":", nonce-value, ":", cnonce-value) |
|
147 local A2 |
|
148 |
|
149 local response_value = HEX(KD(HEX(H(A1)), response["nonce"]..":"..response["nonce-count"]..":"..response["cnonce-value"]..":"..response["qop"]..":"..HEX(H(A2)))) |
|
150 |
|
151 if response["qop"] == "auth" then |
|
152 |
|
153 else |
|
154 |
|
155 end |
|
156 |
|
157 local response_value = HEX(KD(HEX(H(A1)), response["nonce"]..":"..response["nonce-count"]..":"..response["cnonce-value"]..":"..response["qop"]..":"..HEX(H(A2)))) |
|
158 |
|
159 end |
|
160 --[[ |
|
161 local authorization = s_match(response, "([^&%z]+)") |
|
162 local authentication = s_match(response, "%z([^&%z]+)%z") |
|
163 local password = s_match(response, "%z[^&%z]+%z([^&%z]+)") |
|
164 if self.onAuth(authentication, password) == true then |
|
165 self.onWrite(st.stanza("success", {xmlns = "urn:ietf:params:xml:ns:xmpp-sasl"})) |
|
166 self.onSuccess(authentication) |
|
167 else |
|
168 self.onWrite(st.stanza("failure", {xmlns = "urn:ietf:params:xml:ns:xmpp-sasl"}):tag("temporary-auth-failure")); |
|
169 end]]-- |
|
170 end |
|
171 return object |
|
172 end |
|
173 |
33 function new(mechanism, onAuth, onSuccess, onFail, onWrite) |
174 function new(mechanism, onAuth, onSuccess, onFail, onWrite) |
34 local object |
175 local object |
35 if mechanism == "PLAIN" then object = new_plain(onAuth, onSuccess, onFail, onWrite) |
176 if mechanism == "PLAIN" then object = new_plain(onAuth, onSuccess, onFail, onWrite) |
|
177 elseif mechanism == "DIGEST-MD5" then object = new_digest_md5(onAuth, onSuccess, onFail, onWrite) |
36 else |
178 else |
37 log("debug", "Unsupported SASL mechanism: "..tostring(mechanism)); |
179 log("debug", "Unsupported SASL mechanism: "..tostring(mechanism)); |
38 onFail("unsupported-mechanism") |
180 onFail("unsupported-mechanism") |
39 end |
181 end |
40 return object |
182 return object |