util/sasl/scram.lua

1
local base64, unbase64 = require "mime".b64, require"mime".unb64;
2
local hashes = require"prosody.util.hashes";
3
local XOR = require "prosody.util.strbitop".sxor;
4
local random = require"prosody.util.random";
5
 
6
local tonumber = tonumber;
7
local gsub = string.gsub;
8
 
9
local H, HMAC = hashes.sha1, hashes.hmac_sha1;
10
 
11
local function Hi(str, salt, i)
12
	local U = HMAC(str, salt .. "\0\0\0\1");
13
	local ret = U;
14
	for _ = 2, i do
15
		U = HMAC(str, U);
16
		ret = XOR(ret, U);
17
	end
18
	return ret;
19
end
20
 
21
local function Normalize(str)
22
	return str; -- TODO
23
end
24
 
25
local function value_safe(str)
26
	return (gsub(str, "[,=]", { [","] = "=2C", ["="] = "=3D" }));
27
end
28
 
29
local function cb(conn)
30
	if conn:ssl() then
31
		local sock = conn:socket();
32
		if sock.info and sock:info().protocol == "TLSv1.3" then
33
			if sock.exportkeyingmaterial then
34
				return "p=tls-exporter", sock:exportkeyingmaterial("EXPORTER-Channel-Binding", 32, "");
35
			end
36
		elseif sock.getfinished then
37
			return "p=tls-unique", sock:getfinished();
38
		end
39
	end
40
end
41
 
42
local function scram(stream, name)
43
	local username = "n=" .. value_safe(stream.username);
44
	local c_nonce = base64(random.bytes(15));
45
	local our_nonce = "r=" .. c_nonce;
46
	local client_first_message_bare = username .. "," .. our_nonce;
47
	local cbind_data = "";
48
	local gs2_cbind_flag = "n";
49
	if name == "SCRAM-SHA-1-PLUS" then
50
		gs2_cbind_flag, cbind_data = cb(stream.conn);
51
	elseif cb(stream.conn) then
52
		gs2_cbind_flag = "y";
53
	end
54
	local gs2_header = gs2_cbind_flag .. ",,";
55
	local client_first_message = gs2_header .. client_first_message_bare;
56
	local cont, server_first_message = coroutine.yield(client_first_message);
57
	if cont ~= "challenge" then return false end
58
 
59
	local nonce, salt, iteration_count = server_first_message:match("(r=[^,]+),s=([^,]*),i=(%d+)");
60
	local i = tonumber(iteration_count);
61
	salt = unbase64(salt);
62
	if not nonce or not salt or not i then
63
		return false, "Could not parse server_first_message";
64
	elseif nonce:find(c_nonce, 3, true) ~= 3 then
65
		return false, "nonce sent by server does not match our nonce";
66
	elseif nonce == our_nonce then
67
		return false, "server did not append s-nonce to nonce";
68
	end
69
 
70
	local cbind_input = gs2_header .. cbind_data;
71
	local channel_binding = "c=" .. base64(cbind_input);
72
	local client_final_message_without_proof = channel_binding .. "," .. nonce;
73
 
74
	local SaltedPassword;
75
	local ClientKey;
76
	local ServerKey;
77
 
78
	if stream.client_key and stream.server_key then
79
		ClientKey = stream.client_key;
80
		ServerKey = stream.server_key;
81
	else
82
		if stream.salted_password then
83
			SaltedPassword = stream.salted_password;
84
		elseif stream.password then
85
			SaltedPassword = Hi(Normalize(stream.password), salt, i);
86
		end
87
		ServerKey = HMAC(SaltedPassword, "Server Key");
88
		ClientKey = HMAC(SaltedPassword, "Client Key");
89
	end
90
 
91
	local StoredKey       = H(ClientKey);
92
	local AuthMessage     = client_first_message_bare .. "," ..  server_first_message .. "," ..  client_final_message_without_proof;
93
	local ClientSignature = HMAC(StoredKey, AuthMessage);
94
	local ClientProof     = XOR(ClientKey, ClientSignature);
95
	local ServerSignature = HMAC(ServerKey, AuthMessage);
96
 
97
	local proof = "p=" .. base64(ClientProof);
98
	local client_final_message = client_final_message_without_proof .. "," .. proof;
99
 
100
	local ok, server_final_message = coroutine.yield(client_final_message);
101
	if ok ~= "success" then return false, "success-expected" end
102
 
103
	local verifier = server_final_message:match("v=([^,]+)");
104
	if unbase64(verifier) ~= ServerSignature then
105
		return false, "server signature did not match";
106
	end
107
	return true;
108
end
109
 
110
return function (stream, name)
111
	if stream.username and (stream.password or (stream.client_key or stream.server_key)) then
112
		if name == "SCRAM-SHA-1" then
113
			return scram, 99;
114
		elseif name == "SCRAM-SHA-1-PLUS" then
115
			if cb(stream.conn) then
116
				return scram, 100;
117
			end
118
		end
119
	end
120
end