util/sasl.lua

changeset 276
30893439d5d1
parent 50
56272224ca4c
child 277
00c2fc751f50
equal deleted inserted replaced
230:e46525f5b2a4 276:30893439d5d1
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

mercurial