2 -- Copyright (C) 2008-2010 Tobias Markmann
4 -- All rights reserved.
6 -- Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
8 -- * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
9 -- * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
10 -- * Neither the name of Tobias Markmann nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
12 -- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14 local s_match = string.match;
17 local base64 = require "util.encodings".base64;
18 local hmac_sha1 = require "util.hmac".sha1;
19 local sha1 = require "util.hashes".sha1;
20 local generate_uuid = require "util.uuid".generate;
21 local saslprep = require "util.encodings".stringprep.saslprep;
22 local log = require "util.logger".init("sasl");
23 local t_concat = table.concat;
24 local char = string.char;
25 local byte = string.byte;
29 --=========================
30 --SASL SCRAM-SHA-1 according to draft-ietf-sasl-scram-10
33 Supported Authentication Backends
36 -- MECH being a standard hash name (like those at IANA's hash registry) with '-' replaced with '_'
37 function(username, realm)
38 return salted_password, iteration_count, salt, state;
42 local default_i = 4096
44 local function bp( b )
47 result = result.."\\"..b:byte(i)
52 local xor_map = {0;1;2;3;4;5;6;7;8;9;10;11;12;13;14;15;1;0;3;2;5;4;7;6;9;8;11;10;13;12;15;14;2;3;0;1;6;7;4;5;10;11;8;9;14;15;12;13;3;2;1;0;7;6;5;4;11;10;9;8;15;14;13;12;4;5;6;7;0;1;2;3;12;13;14;15;8;9;10;11;5;4;7;6;1;0;3;2;13;12;15;14;9;8;11;10;6;7;4;5;2;3;0;1;14;15;12;13;10;11;8;9;7;6;5;4;3;2;1;0;15;14;13;12;11;10;9;8;8;9;10;11;12;13;14;15;0;1;2;3;4;5;6;7;9;8;11;10;13;12;15;14;1;0;3;2;5;4;7;6;10;11;8;9;14;15;12;13;2;3;0;1;6;7;4;5;11;10;9;8;15;14;13;12;3;2;1;0;7;6;5;4;12;13;14;15;8;9;10;11;4;5;6;7;0;1;2;3;13;12;15;14;9;8;11;10;5;4;7;6;1;0;3;2;14;15;12;13;10;11;8;9;6;7;4;5;2;3;0;1;15;14;13;12;11;10;9;8;7;6;5;4;3;2;1;0;};
55 local function binaryXOR( a, b )
57 local x, y = byte(a, i), byte(b, i);
58 local lowx, lowy = x % 16, y % 16;
59 local hix, hiy = (x - lowx) / 16, (y - lowy) / 16;
60 local lowr, hir = xor_map[lowx * 16 + lowy + 1], xor_map[hix * 16 + hiy + 1];
61 local r = hir * 16 + lowr;
64 return t_concat(result);
67 -- hash algorithm independent Hi(PBKDF2) implementation
68 local function Hi(hmac, str, salt, i)
69 local Ust = hmac(str, salt.."\0\0\0\1");
72 local Und = hmac(str, Ust)
73 res = binaryXOR(res, Und)
79 local function validate_username(username)
80 -- check for forbidden char sequences
81 for eq in username:gmatch("=(.?.?)") do
82 if eq ~= "2D" and eq ~= "3D" then
87 -- replace =2D with , and =3D with =
88 username = username:gsub("=2D", ",");
89 username = username:gsub("=3D", "=");
92 username = saslprep(username);
96 local function hashprep( hashname )
97 local hash = hashname:lower()
98 hash = hash:gsub("-", "_")
102 function saltedPasswordSHA1(password, salt, iteration_count)
103 local salted_password
104 if type(password) ~= "string" or type(salt) ~= "string" or type(iteration_count) ~= "number" then
105 return false, "inappropriate argument types"
107 if iteration_count < 4096 then
108 log("warning", "Iteration count < 4096 which is the suggested minimum according to RFC 5802.")
111 return true, Hi(hmac_sha1, password, salt, iteration_count);
114 local function scram_gen(hash_name, H_f, HMAC_f)
115 local function scram_hash(self, message)
116 if not self.state then self["state"] = {} end
118 if type(message) ~= "string" or #message == 0 then return "failure", "malformed-request" end
119 if not self.state.name then
120 -- we are processing client_first_message
121 local client_first_message = message;
123 -- TODO: fail if authzid is provided, since we don't support them yet
124 self.state["client_first_message"] = client_first_message;
125 self.state["gs2_cbind_flag"], self.state["authzid"], self.state["name"], self.state["clientnonce"]
126 = client_first_message:match("^(%a),(.*),n=(.*),r=([^,]*).*");
128 -- we don't do any channel binding yet
129 if self.state.gs2_cbind_flag ~= "n" and self.state.gs2_cbind_flag ~= "y" then
130 return "failure", "malformed-request";
133 if not self.state.name or not self.state.clientnonce then
134 return "failure", "malformed-request", "Channel binding isn't support at this time.";
137 self.state.name = validate_username(self.state.name);
138 if not self.state.name then
139 log("debug", "Username violates either SASLprep or contains forbidden character sequences.")
140 return "failure", "malformed-request", "Invalid username.";
143 self.state["servernonce"] = generate_uuid();
145 -- retreive credentials
146 if self.profile.plain then
147 local password, state = self.profile.plain(self.state.name, self.realm)
148 if state == nil then return "failure", "not-authorized"
149 elseif state == false then return "failure", "account-disabled" end
151 password = saslprep(password);
153 log("debug", "Password violates SASLprep.");
154 return "failure", "not-authorized", "Invalid password."
157 self.state.salt = generate_uuid();
158 self.state.iteration_count = default_i;
161 succ, self.state.salted_password = saltedPasswordSHA1(password, self.state.salt, default_i, self.state.iteration_count);
163 log("error", "Generating salted password failed. Reason: %s", self.state.salted_password);
164 return "failure", "temporary-auth-failure";
166 elseif self.profile["scram_"..hashprep(hash_name)] then
167 local salted_password, iteration_count, salt, state = self.profile["scram-"..hash_name](self.state.name, self.realm);
168 if state == nil then return "failure", "not-authorized"
169 elseif state == false then return "failure", "account-disabled" end
171 self.state.salted_password = salted_password;
172 self.state.iteration_count = iteration_count;
173 self.state.salt = salt
176 local server_first_message = "r="..self.state.clientnonce..self.state.servernonce..",s="..base64.encode(self.state.salt)..",i="..self.state.iteration_count;
177 self.state["server_first_message"] = server_first_message;
178 return "challenge", server_first_message
180 -- we are processing client_final_message
181 local client_final_message = message;
183 self.state["channelbinding"], self.state["nonce"], self.state["proof"] = client_final_message:match("^c=(.*),r=(.*),.*p=(.*)");
185 if not self.state.proof or not self.state.nonce or not self.state.channelbinding then
186 return "failure", "malformed-request", "Missing an attribute(p, r or c) in SASL message.";
189 if self.state.nonce ~= self.state.clientnonce..self.state.servernonce then
190 return "failure", "malformed-request", "Wrong nonce in client-final-message.";
193 local SaltedPassword = self.state.salted_password;
194 local ClientKey = HMAC_f(SaltedPassword, "Client Key")
195 local ServerKey = HMAC_f(SaltedPassword, "Server Key")
196 local StoredKey = H_f(ClientKey)
197 local AuthMessage = "n=" .. s_match(self.state.client_first_message,"n=(.+)") .. "," .. self.state.server_first_message .. "," .. s_match(client_final_message, "(.+),p=.+")
198 local ClientSignature = HMAC_f(StoredKey, AuthMessage)
199 local ClientProof = binaryXOR(ClientKey, ClientSignature)
200 local ServerSignature = HMAC_f(ServerKey, AuthMessage)
202 if base64.encode(ClientProof) == self.state.proof then
203 local server_final_message = "v="..base64.encode(ServerSignature);
204 self["username"] = self.state.name;
205 return "success", server_final_message;
207 return "failure", "not-authorized", "The response provided by the client doesn't match the one we calculated.";
214 function init(registerMechanism)
215 local function registerSCRAMMechanism(hash_name, hash, hmac_hash)
216 registerMechanism("SCRAM-"..hash_name, {"plain", "scram_"..(hashprep(hash_name))}, scram_gen(hash_name:lower(), hash, hmac_hash));
219 registerSCRAMMechanism("SHA-1", sha1, hmac_sha1);