1 |
|
-module(mongoose_scram). |
2 |
|
|
3 |
|
-include("mongoose.hrl"). |
4 |
|
-include("scram.hrl"). |
5 |
|
|
6 |
|
% Core SCRAM functions |
7 |
|
-export([salted_password/4]). |
8 |
|
|
9 |
|
-export([ |
10 |
|
enabled/1, |
11 |
|
enabled/2, |
12 |
|
iterations/0, |
13 |
|
iterations/1, |
14 |
|
password_to_scram/2, |
15 |
|
password_to_scram/3, |
16 |
|
password_to_scram_sha/3, |
17 |
|
check_password/2, |
18 |
|
check_digest/4 |
19 |
|
]). |
20 |
|
|
21 |
|
-export([serialize/1, deserialize/1]). |
22 |
|
|
23 |
|
-export([scram_to_tuple/1, scram_record_to_map/1]). |
24 |
|
|
25 |
|
-ignore_xref([password_to_scram/2, scram_to_tuple/1]). |
26 |
|
|
27 |
|
-type scram_tuple() :: { StoredKey :: binary(), ServerKey :: binary(), |
28 |
|
Salt :: binary(), Iterations :: non_neg_integer() }. |
29 |
|
|
30 |
|
-type scram_map() :: |
31 |
|
#{iteration_count := non_neg_integer(), |
32 |
|
sha_key() := server_and_stored_key_type()}. |
33 |
|
|
34 |
|
-type sha_key() :: sha | sha224 | sha256 | sha384 | sha512. |
35 |
|
|
36 |
|
-type server_and_stored_key_type() :: #{salt := binary(), |
37 |
|
server_key := binary(), |
38 |
|
stored_key := binary()}. |
39 |
|
|
40 |
|
-type scram() :: #scram{}. |
41 |
|
|
42 |
|
-export_type([scram_tuple/0, scram/0, scram_map/0]). |
43 |
|
|
44 |
|
-define(SALT_LENGTH, 16). |
45 |
|
-define(SCRAM_DEFAULT_ITERATION_COUNT, 10000). |
46 |
|
-define(SCRAM_SERIAL_PREFIX, "==SCRAM==,"). |
47 |
|
-define(MULTI_SCRAM_SERIAL_PREFIX, "==MULTI_SCRAM==,"). |
48 |
|
-define(SCRAM_SHA1_PREFIX, "===SHA1==="). |
49 |
|
-define(SCRAM_SHA224_PREFIX, "==SHA224=="). |
50 |
|
-define(SCRAM_SHA256_PREFIX, "==SHA256=="). |
51 |
|
-define(SCRAM_SHA384_PREFIX, "==SHA384=="). |
52 |
|
-define(SCRAM_SHA512_PREFIX, "==SHA512=="). |
53 |
|
|
54 |
|
%% ejabberd doesn't implement SASLPREP, so we use the similar RESOURCEPREP instead |
55 |
|
salted_password(Sha, Password, Salt, IterationCount) -> |
56 |
13870 |
fast_scram:salted_password(Sha, jid:resourceprep(Password), Salt, IterationCount). |
57 |
|
|
58 |
|
enabled(HostType) -> |
59 |
6108 |
mongoose_config:get_opt([{auth, HostType}, password, format]) =:= scram. |
60 |
|
|
61 |
13532 |
enabled(HostType, cyrsasl_scram_sha1) -> is_password_format_allowed(HostType, sha); |
62 |
13521 |
enabled(HostType, cyrsasl_scram_sha224) -> is_password_format_allowed(HostType, sha224); |
63 |
13526 |
enabled(HostType, cyrsasl_scram_sha256) -> is_password_format_allowed(HostType, sha256); |
64 |
13521 |
enabled(HostType, cyrsasl_scram_sha384) -> is_password_format_allowed(HostType, sha384); |
65 |
13521 |
enabled(HostType, cyrsasl_scram_sha512) -> is_password_format_allowed(HostType, sha512); |
66 |
13515 |
enabled(HostType, cyrsasl_scram_sha1_plus) -> is_password_format_allowed(HostType, sha); |
67 |
13515 |
enabled(HostType, cyrsasl_scram_sha224_plus) -> is_password_format_allowed(HostType, sha224); |
68 |
13515 |
enabled(HostType, cyrsasl_scram_sha256_plus) -> is_password_format_allowed(HostType, sha256); |
69 |
13515 |
enabled(HostType, cyrsasl_scram_sha384_plus) -> is_password_format_allowed(HostType, sha384); |
70 |
13515 |
enabled(HostType, cyrsasl_scram_sha512_plus) -> is_password_format_allowed(HostType, sha512); |
71 |
13506 |
enabled(_HostType, _Mechanism) -> false. |
72 |
|
|
73 |
|
is_password_format_allowed(HostType, Sha) -> |
74 |
135196 |
case mongoose_config:get_opt([{auth, HostType}, password]) of |
75 |
130965 |
#{format := scram, hash := ConfiguredSha} -> lists:member(Sha, ConfiguredSha); |
76 |
4231 |
#{format := _PlainOrScram} -> true |
77 |
|
end. |
78 |
|
|
79 |
|
%% This function is exported and used from other modules |
80 |
261 |
iterations() -> ?SCRAM_DEFAULT_ITERATION_COUNT. |
81 |
|
|
82 |
|
iterations(HostType) -> |
83 |
6069 |
mongoose_config:get_opt([{auth, HostType}, password, scram_iterations]). |
84 |
|
|
85 |
|
password_to_scram_sha(Password, IterationCount, HashType) -> |
86 |
31 |
ScramHash = do_password_to_scram(Password, IterationCount, HashType), |
87 |
31 |
maps:from_list([{iteration_count, IterationCount}, ScramHash]). |
88 |
|
|
89 |
|
password_to_scram(HostType, Password) -> |
90 |
:-( |
password_to_scram(HostType, Password, ?SCRAM_DEFAULT_ITERATION_COUNT). |
91 |
|
|
92 |
|
password_to_scram(_, #scram{} = Password, _) -> |
93 |
:-( |
scram_record_to_map(Password); |
94 |
|
password_to_scram(HostType, Password, IterationCount) -> |
95 |
6069 |
ServerStoredKeys = [do_password_to_scram(Password, IterationCount, HashType) |
96 |
6069 |
|| {HashType, _Prefix} <- configured_sha_types(HostType)], |
97 |
6069 |
ResultList = lists:merge([{iteration_count, IterationCount}], ServerStoredKeys), |
98 |
6069 |
maps:from_list(ResultList). |
99 |
|
|
100 |
|
do_password_to_scram(Password, IterationCount, HashType) -> |
101 |
6456 |
Salt = crypto:strong_rand_bytes(?SALT_LENGTH), |
102 |
6456 |
SaltedPassword = salted_password(HashType, Password, Salt, IterationCount), |
103 |
6456 |
StoredKey = fast_scram:stored_key(HashType, fast_scram:client_key(HashType, SaltedPassword)), |
104 |
6456 |
ServerKey = fast_scram:server_key(HashType, SaltedPassword), |
105 |
6456 |
{HashType, #{salt => base64:encode(Salt), |
106 |
|
server_key => base64:encode(ServerKey), |
107 |
|
stored_key => base64:encode(StoredKey)}}. |
108 |
|
|
109 |
|
check_password(Password, Scram) when is_record(Scram, scram)-> |
110 |
:-( |
ScramMap = scram_record_to_map(Scram), |
111 |
:-( |
check_password(Password, ScramMap); |
112 |
|
check_password(Password, ScramMap) when is_map(ScramMap) -> |
113 |
7403 |
#{iteration_count := IterationCount} = ScramMap, |
114 |
7403 |
[Sha | _] = [ShaKey || {ShaKey, _Prefix} <- supported_sha_types(), |
115 |
37015 |
maps:is_key(ShaKey, ScramMap)], |
116 |
7403 |
#{Sha := #{salt := Salt, stored_key := StoredKey}} = ScramMap, |
117 |
7403 |
SaltedPassword = salted_password(Sha, Password, base64:decode(Salt), IterationCount), |
118 |
7403 |
ClientStoredKey = fast_scram:stored_key(Sha, fast_scram:client_key(Sha, SaltedPassword)), |
119 |
7403 |
ClientStoredKey == base64:decode(StoredKey). |
120 |
|
|
121 |
|
serialize(#scram{storedkey = StoredKey, serverkey = ServerKey, |
122 |
|
salt = Salt, iterationcount = IterationCount})-> |
123 |
:-( |
IterationCountBin = integer_to_binary(IterationCount), |
124 |
:-( |
<< <<?SCRAM_SERIAL_PREFIX>>/binary, |
125 |
|
StoredKey/binary, $,, ServerKey/binary, |
126 |
|
$,, Salt/binary, $,, IterationCountBin/binary>>; |
127 |
|
serialize(#{iteration_count := IterationCount} = ScramMap) -> |
128 |
6071 |
IterationCountBin = integer_to_binary(IterationCount), |
129 |
6071 |
ConfigedSha = [{ShaKey, Prefix} || {ShaKey, Prefix} <- supported_sha_types(), |
130 |
30355 |
maps:is_key(ShaKey, ScramMap)], |
131 |
6071 |
Header = [?MULTI_SCRAM_SERIAL_PREFIX, IterationCountBin], |
132 |
6071 |
do_serialize(Header, ScramMap, ConfigedSha). |
133 |
|
|
134 |
|
do_serialize(Serialized, _ ,[]) -> |
135 |
6071 |
erlang:iolist_to_binary(Serialized); |
136 |
|
do_serialize(Header, ScramMap, [{Sha, Prefix} | RemainingSha]) -> |
137 |
6311 |
#{Sha := #{salt := Salt, |
138 |
|
server_key := ServerKey, |
139 |
|
stored_key := StoredKey}} = ScramMap, |
140 |
6311 |
ShaSerialization = [$, , Prefix, Salt, $|, StoredKey, $|, ServerKey], |
141 |
6311 |
NewHeader = [Header | ShaSerialization], |
142 |
6311 |
do_serialize(NewHeader, ScramMap, RemainingSha). |
143 |
|
|
144 |
|
deserialize(<<?SCRAM_SERIAL_PREFIX, Serialized/binary>>) -> |
145 |
:-( |
case catch binary:split(Serialized, <<",">>, [global]) of |
146 |
|
[StoredKey, ServerKey, Salt, IterationCount] -> |
147 |
:-( |
{ok, #{iteration_count => binary_to_integer(IterationCount), |
148 |
|
sha => #{salt => Salt, |
149 |
|
stored_key => StoredKey, |
150 |
|
server_key => ServerKey}}}; |
151 |
|
_ -> |
152 |
:-( |
?LOG_WARNING(#{what => scram_serialisation_incorrect}), |
153 |
:-( |
{error, incorrect_scram} |
154 |
|
end; |
155 |
|
deserialize(<<?MULTI_SCRAM_SERIAL_PREFIX, Serialized/binary>>) -> |
156 |
7430 |
case catch binary:split(Serialized, <<",">>, [global]) of |
157 |
|
[IterationCountBin | ListOfShaSpecificDetails] -> |
158 |
7430 |
IterationCount = binary_to_integer(IterationCountBin), |
159 |
7430 |
DeserializedKeys = [deserialize(supported_sha_types(), ShaDetails) |
160 |
7430 |
|| ShaDetails <- ListOfShaSpecificDetails], |
161 |
7430 |
ResultList = lists:merge([{iteration_count, IterationCount}], |
162 |
|
lists:flatten(DeserializedKeys)), |
163 |
7430 |
{ok, maps:from_list(ResultList)}; |
164 |
|
_ -> |
165 |
:-( |
?LOG_WARNING(#{what => scram_serialisation_incorrect}), |
166 |
:-( |
{error, incorrect_scram} |
167 |
|
end; |
168 |
|
deserialize(_) -> |
169 |
:-( |
?LOG_WARNING(#{what => scram_serialisation_corrupted}), |
170 |
:-( |
{error, corrupted_scram}. |
171 |
|
|
172 |
|
deserialize([], _) -> |
173 |
:-( |
[]; |
174 |
|
deserialize([{Sha, Prefix} | _RemainingSha], |
175 |
|
<<Prefix:10/binary, ShaDetails/binary>>) -> |
176 |
7910 |
case catch binary:split(ShaDetails, <<"|">>, [global]) of |
177 |
|
[Salt, StoredKey, ServerKey] -> |
178 |
7910 |
{Sha, #{salt => Salt, server_key => ServerKey, stored_key => StoredKey}}; |
179 |
|
_ -> |
180 |
:-( |
?LOG_WARNING(#{what => scram_serialisation_incorrect}) |
181 |
|
end; |
182 |
|
deserialize([_CurrentSha | RemainingSha], ShaDetails) -> |
183 |
16754 |
deserialize(RemainingSha, ShaDetails). |
184 |
|
|
185 |
|
-spec scram_to_tuple(scram()) -> scram_tuple(). |
186 |
|
scram_to_tuple(Scram) -> |
187 |
:-( |
{base64:decode(Scram#scram.storedkey), |
188 |
|
base64:decode(Scram#scram.serverkey), |
189 |
|
base64:decode(Scram#scram.salt), |
190 |
|
Scram#scram.iterationcount}. |
191 |
|
|
192 |
|
-spec scram_record_to_map(scram()) -> scram_map(). |
193 |
|
scram_record_to_map(Scram) -> |
194 |
:-( |
#{iteration_count => Scram#scram.iterationcount, |
195 |
|
sha => #{salt => Scram#scram.salt, |
196 |
|
stored_key => Scram#scram.storedkey, |
197 |
|
server_key => Scram#scram.serverkey}}. |
198 |
|
|
199 |
|
-spec check_digest(Scram, binary(), fun(), binary()) -> boolean() when |
200 |
|
Scram :: scram_map() | scram(). |
201 |
|
check_digest(Scram, Digest, DigestGen, Password) when is_record(Scram, scram) -> |
202 |
:-( |
ScramMap = scram_record_to_map(Scram), |
203 |
:-( |
check_digest(ScramMap, Digest, DigestGen, Password); |
204 |
|
check_digest(ScramMap, Digest, DigestGen, Password) -> |
205 |
:-( |
do_check_digest(supported_sha_types(), ScramMap, Digest, DigestGen, Password). |
206 |
|
|
207 |
|
do_check_digest([] , _, _, _, _) -> |
208 |
:-( |
false; |
209 |
|
do_check_digest([{Sha,_Prefix} | RemainingSha], ScramMap, Digest, DigestGen, Password) -> |
210 |
:-( |
#{Sha := #{stored_key := StoredKey}} = ScramMap, |
211 |
:-( |
Passwd = base64:decode(StoredKey), |
212 |
:-( |
case ejabberd_auth:check_digest(Digest, DigestGen, Password, Passwd) of |
213 |
:-( |
true -> true; |
214 |
:-( |
false -> do_check_digest(RemainingSha, Digest, DigestGen, Password, Passwd) |
215 |
|
end. |
216 |
|
|
217 |
|
supported_sha_types() -> |
218 |
27453 |
[{sha, <<?SCRAM_SHA1_PREFIX>>}, |
219 |
|
{sha224, <<?SCRAM_SHA224_PREFIX>>}, |
220 |
|
{sha256, <<?SCRAM_SHA256_PREFIX>>}, |
221 |
|
{sha384, <<?SCRAM_SHA384_PREFIX>>}, |
222 |
|
{sha512, <<?SCRAM_SHA512_PREFIX>>}]. |
223 |
|
|
224 |
|
configured_sha_types(HostType) -> |
225 |
6069 |
case mongoose_config:lookup_opt([{auth, HostType}, password, hash]) of |
226 |
|
{ok, ScramSha} -> |
227 |
5980 |
lists:filter(fun({Sha, _Prefix}) -> |
228 |
29900 |
lists:member(Sha, ScramSha) end, supported_sha_types()); |
229 |
89 |
_ -> supported_sha_types() |
230 |
|
end. |