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 stored_key, server_key, 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 return hashname:lower():gsub("-", "_");
100 function getAuthenticationDatabaseSHA1(password, salt, iteration_count)
101 if type(password) ~= "string" or type(salt) ~= "string" or type(iteration_count) ~= "number" then
102 return false, "inappropriate argument types"
104 if iteration_count < 4096 then
105 log("warn", "Iteration count < 4096 which is the suggested minimum according to RFC 5802.")
107 local salted_password = Hi(hmac_sha1, password, salt, iteration_count);
108 local stored_key = sha1(hmac_sha1(salted_password, "Client Key"))
109 local server_key = hmac_sha1(salted_password, "Server Key");
110 return true, stored_key, server_key
113 local function scram_gen(hash_name, H_f, HMAC_f)
114 local function scram_hash(self, message)
115 if not self.state then self["state"] = {} end
117 if type(message) ~= "string" or #message == 0 then return "failure", "malformed-request" end
118 if not self.state.name then
119 -- we are processing client_first_message
120 local client_first_message = message;
122 -- TODO: fail if authzid is provided, since we don't support them yet
123 self.state["client_first_message"] = client_first_message;
124 self.state["gs2_cbind_flag"], self.state["authzid"], self.state["name"], self.state["clientnonce"]
125 = client_first_message:match("^(%a),(.*),n=(.*),r=([^,]*).*");
127 -- we don't do any channel binding yet
128 if self.state.gs2_cbind_flag ~= "n" and self.state.gs2_cbind_flag ~= "y" then
129 return "failure", "malformed-request";
132 if not self.state.name or not self.state.clientnonce then
133 return "failure", "malformed-request", "Channel binding isn't support at this time.";
136 self.state.name = validate_username(self.state.name);
137 if not self.state.name then
138 log("debug", "Username violates either SASLprep or contains forbidden character sequences.")
139 return "failure", "malformed-request", "Invalid username.";
142 self.state["servernonce"] = generate_uuid();
144 -- retreive credentials
145 if self.profile.plain then
146 local password, state = self.profile.plain(self.state.name, self.realm)
147 if state == nil then return "failure", "not-authorized"
148 elseif state == false then return "failure", "account-disabled" end
150 password = saslprep(password);
152 log("debug", "Password violates SASLprep.");
153 return "failure", "not-authorized", "Invalid password."
156 self.state.salt = generate_uuid();
157 self.state.iteration_count = default_i;
160 succ, self.state.stored_key, self.state.server_key = getAuthenticationDatabaseSHA1(password, self.state.salt, default_i, self.state.iteration_count);
162 log("error", "Generating authentication database failed. Reason: %s", self.state.stored_key);
163 return "failure", "temporary-auth-failure";
165 elseif self.profile["scram_"..hashprep(hash_name)] then
166 local stored_key, server_key, iteration_count, salt, state = self.profile["scram_"..hashprep(hash_name)](self.state.name, self.realm);
167 if state == nil then return "failure", "not-authorized"
168 elseif state == false then return "failure", "account-disabled" end
170 self.state.stored_key = stored_key;
171 self.state.server_key = server_key;
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 ServerKey = self.state.server_key;
194 local StoredKey = self.state.stored_key;
196 local AuthMessage = "n=" .. s_match(self.state.client_first_message,"n=(.+)") .. "," .. self.state.server_first_message .. "," .. s_match(client_final_message, "(.+),p=.+")
197 local ClientSignature = HMAC_f(StoredKey, AuthMessage)
198 local ClientKey = binaryXOR(ClientSignature, base64.decode(self.state.proof))
199 local ServerSignature = HMAC_f(ServerKey, AuthMessage)
201 if StoredKey == H_f(ClientKey) then
202 local server_final_message = "v="..base64.encode(ServerSignature);
203 self["username"] = self.state.name;
204 return "success", server_final_message;
206 return "failure", "not-authorized", "The response provided by the client doesn't match the one we calculated.";
213 function init(registerMechanism)
214 local function registerSCRAMMechanism(hash_name, hash, hmac_hash)
215 registerMechanism("SCRAM-"..hash_name, {"plain", "scram_"..(hashprep(hash_name))}, scram_gen(hash_name:lower(), hash, hmac_hash));
218 registerSCRAMMechanism("SHA-1", sha1, hmac_sha1);