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.hashes".hmac_sha1;
19 local sha1 = require "util.hashes".sha1;
20 local Hi = require "util.hashes".scram_Hi_sha1;
21 local generate_uuid = require "util.uuid".generate;
22 local saslprep = require "util.encodings".stringprep.saslprep;
23 local nodeprep = require "util.encodings".stringprep.nodeprep;
24 local log = require "util.logger".init("sasl");
25 local t_concat = table.concat;
26 local char = string.char;
27 local byte = string.byte;
31 --=========================
32 --SASL SCRAM-SHA-1 according to RFC 5802
35 Supported Authentication Backends
38 -- MECH being a standard hash name (like those at IANA's hash registry) with '-' replaced with '_'
39 function(username, realm)
40 return stored_key, server_key, iteration_count, salt, state;
44 local default_i = 4096
46 local function bp( b )
49 result = result.."\\"..b:byte(i)
54 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;};
57 local function binaryXOR( a, b )
59 local x, y = byte(a, i), byte(b, i);
60 local lowx, lowy = x % 16, y % 16;
61 local hix, hiy = (x - lowx) / 16, (y - lowy) / 16;
62 local lowr, hir = xor_map[lowx * 16 + lowy + 1], xor_map[hix * 16 + hiy + 1];
63 local r = hir * 16 + lowr;
66 return t_concat(result);
69 local function validate_username(username, _nodeprep)
70 -- check for forbidden char sequences
71 for eq in username:gmatch("=(.?.?)") do
72 if eq ~= "2C" and eq ~= "3D" then
77 -- replace =2C with , and =3D with =
78 username = username:gsub("=2C", ",");
79 username = username:gsub("=3D", "=");
82 username = saslprep(username);
84 if username and _nodeprep ~= false then
85 username = (_nodeprep or nodeprep)(username);
88 return username and #username>0 and username;
91 local function hashprep(hashname)
92 return hashname:lower():gsub("-", "_");
95 function getAuthenticationDatabaseSHA1(password, salt, iteration_count)
96 if type(password) ~= "string" or type(salt) ~= "string" or type(iteration_count) ~= "number" then
97 return false, "inappropriate argument types"
99 if iteration_count < 4096 then
100 log("warn", "Iteration count < 4096 which is the suggested minimum according to RFC 5802.")
102 local salted_password = Hi(password, salt, iteration_count);
103 local stored_key = sha1(hmac_sha1(salted_password, "Client Key"))
104 local server_key = hmac_sha1(salted_password, "Server Key");
105 return true, stored_key, server_key
108 local function scram_gen(hash_name, H_f, HMAC_f)
109 local function scram_hash(self, message)
110 if not self.state then self["state"] = {} end
112 if type(message) ~= "string" or #message == 0 then return "failure", "malformed-request" end
113 if not self.state.name then
114 -- we are processing client_first_message
115 local client_first_message = message;
117 -- TODO: fail if authzid is provided, since we don't support them yet
118 self.state["client_first_message"] = client_first_message;
119 self.state["gs2_cbind_flag"], self.state["authzid"], self.state["name"], self.state["clientnonce"]
120 = client_first_message:match("^(%a),(.*),n=(.*),r=([^,]*).*");
122 -- we don't do any channel binding yet
123 if self.state.gs2_cbind_flag ~= "n" and self.state.gs2_cbind_flag ~= "y" then
124 return "failure", "malformed-request";
127 if not self.state.name or not self.state.clientnonce then
128 return "failure", "malformed-request", "Channel binding isn't support at this time.";
131 self.state.name = validate_username(self.state.name, self.profile.nodeprep);
132 if not self.state.name then
133 log("debug", "Username violates either SASLprep or contains forbidden character sequences.")
134 return "failure", "malformed-request", "Invalid username.";
137 self.state["servernonce"] = generate_uuid();
139 -- retreive credentials
140 if self.profile.plain then
141 local password, state = self.profile.plain(self, self.state.name, self.realm)
142 if state == nil then return "failure", "not-authorized"
143 elseif state == false then return "failure", "account-disabled" end
145 password = saslprep(password);
147 log("debug", "Password violates SASLprep.");
148 return "failure", "not-authorized", "Invalid password."
151 self.state.salt = generate_uuid();
152 self.state.iteration_count = default_i;
155 succ, self.state.stored_key, self.state.server_key = getAuthenticationDatabaseSHA1(password, self.state.salt, default_i, self.state.iteration_count);
157 log("error", "Generating authentication database failed. Reason: %s", self.state.stored_key);
158 return "failure", "temporary-auth-failure";
160 elseif self.profile["scram_"..hashprep(hash_name)] then
161 local stored_key, server_key, iteration_count, salt, state = self.profile["scram_"..hashprep(hash_name)](self, self.state.name, self.realm);
162 if state == nil then return "failure", "not-authorized"
163 elseif state == false then return "failure", "account-disabled" end
165 self.state.stored_key = stored_key;
166 self.state.server_key = server_key;
167 self.state.iteration_count = iteration_count;
168 self.state.salt = salt
171 local server_first_message = "r="..self.state.clientnonce..self.state.servernonce..",s="..base64.encode(self.state.salt)..",i="..self.state.iteration_count;
172 self.state["server_first_message"] = server_first_message;
173 return "challenge", server_first_message
175 -- we are processing client_final_message
176 local client_final_message = message;
178 self.state["channelbinding"], self.state["nonce"], self.state["proof"] = client_final_message:match("^c=(.*),r=(.*),.*p=(.*)");
180 if not self.state.proof or not self.state.nonce or not self.state.channelbinding then
181 return "failure", "malformed-request", "Missing an attribute(p, r or c) in SASL message.";
184 if self.state.nonce ~= self.state.clientnonce..self.state.servernonce then
185 return "failure", "malformed-request", "Wrong nonce in client-final-message.";
188 local ServerKey = self.state.server_key;
189 local StoredKey = self.state.stored_key;
191 local AuthMessage = "n=" .. s_match(self.state.client_first_message,"n=(.+)") .. "," .. self.state.server_first_message .. "," .. s_match(client_final_message, "(.+),p=.+")
192 local ClientSignature = HMAC_f(StoredKey, AuthMessage)
193 local ClientKey = binaryXOR(ClientSignature, base64.decode(self.state.proof))
194 local ServerSignature = HMAC_f(ServerKey, AuthMessage)
196 if StoredKey == H_f(ClientKey) then
197 local server_final_message = "v="..base64.encode(ServerSignature);
198 self["username"] = self.state.name;
199 return "success", server_final_message;
201 return "failure", "not-authorized", "The response provided by the client doesn't match the one we calculated.";
208 function init(registerMechanism)
209 local function registerSCRAMMechanism(hash_name, hash, hmac_hash)
210 registerMechanism("SCRAM-"..hash_name, {"plain", "scram_"..(hashprep(hash_name))}, scram_gen(hash_name:lower(), hash, hmac_hash));
213 registerSCRAMMechanism("SHA-1", sha1, hmac_sha1);