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