util/sasl/scram.lua

1
-- sasl.lua v0.4
2
-- Copyright (C) 2008-2010 Tobias Markmann
3
--
4
--	  All rights reserved.
5
--
6
--	  Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
7
--
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.
11
--
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.
13
 
14
local s_match = string.match;
15
local type = type
16
local string = string
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;
26
 
27
module "scram"
28
 
29
--=========================
30
--SASL SCRAM-SHA-1 according to RFC 5802
31
 
32
--[[
33
Supported Authentication Backends
34
 
35
scram_{MECH}:
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;
39
	end
40
]]
41
 
42
local default_i = 4096
43
 
44
local function bp( b )
45
	local result = ""
46
	for i=1, b:len() do
47
		result = result.."\\"..b:byte(i)
48
	end
49
	return result
50
end
51
 
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;};
53
 
54
local result = {};
55
local function binaryXOR( a, b )
56
	for i=1, #a do
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;
62
		result[i] = char(r)
63
	end
64
	return t_concat(result);
65
end
66
 
67
-- hash algorithm independent Hi(PBKDF2) implementation
68
function Hi(hmac, str, salt, i)
69
	local Ust = hmac(str, salt.."\0\0\0\1");
70
	local res = Ust;
71
	for n=1,i-1 do
72
		local Und = hmac(str, Ust)
73
		res = binaryXOR(res, Und)
74
		Ust = Und
75
	end
76
	return res
77
end
78
 
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
83
			return false
84
		end
85
	end
86
	
87
	-- replace =2D with , and =3D with =
88
	username = username:gsub("=2D", ",");
89
	username = username:gsub("=3D", "=");
90
	
91
	-- apply SASLprep
92
	username = saslprep(username);
93
	return username;
94
end
95
 
96
local function hashprep(hashname)
97
	return hashname:lower():gsub("-", "_");
98
end
99
 
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"
103
	end
104
	if iteration_count < 4096 then
105
		log("warn", "Iteration count < 4096 which is the suggested minimum according to RFC 5802.")
106
	end
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
111
end
112
 
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
116
	
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;
121
			
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=([^,]*).*");
126
 
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";
130
			end
131
 
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.";
134
			end
135
		
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.";
140
			end
141
		
142
			self.state["servernonce"] = generate_uuid();
143
			
144
			-- retreive credentials
145
			if self.profile.plain then
146
				local password, state = self.profile.plain(self, self.state.name, self.realm)
147
				if state == nil then return "failure", "not-authorized"
148
				elseif state == false then return "failure", "account-disabled" end
149
				
150
				password = saslprep(password);
151
				if not password then
152
					log("debug", "Password violates SASLprep.");
153
					return "failure", "not-authorized", "Invalid password."
154
				end
155
 
156
				self.state.salt = generate_uuid();
157
				self.state.iteration_count = default_i;
158
 
159
				local succ = false;
160
				succ, self.state.stored_key, self.state.server_key = getAuthenticationDatabaseSHA1(password, self.state.salt, default_i, self.state.iteration_count);
161
				if not succ then
162
					log("error", "Generating authentication database failed. Reason: %s", self.state.stored_key);
163
					return "failure", "temporary-auth-failure";
164
				end
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, self.state.name, self.realm);
167
				if state == nil then return "failure", "not-authorized"
168
				elseif state == false then return "failure", "account-disabled" end
169
				
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
174
			end
175
		
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
179
		else
180
			-- we are processing client_final_message
181
			local client_final_message = message;
182
			
183
			self.state["channelbinding"], self.state["nonce"], self.state["proof"] = client_final_message:match("^c=(.*),r=(.*),.*p=(.*)");
184
	
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.";
187
			end
188
 
189
			if self.state.nonce ~= self.state.clientnonce..self.state.servernonce then
190
				return "failure", "malformed-request", "Wrong nonce in client-final-message.";
191
			end
192
			
193
			local ServerKey = self.state.server_key;
194
			local StoredKey = self.state.stored_key;
195
			
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)
200
 
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;
205
			else
206
				return "failure", "not-authorized", "The response provided by the client doesn't match the one we calculated.";
207
			end
208
		end
209
	end
210
	return scram_hash;
211
end
212
 
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));
216
	end
217
 
218
	registerSCRAMMechanism("SHA-1", sha1, hmac_sha1);
219
end
220
 
221
return _M;