1 |
|
%%%============================================================================= |
2 |
|
%%% @copyright (C) 1999-2021, Erlang Solutions Ltd |
3 |
|
%%% @doc SASL SCRAM implementation |
4 |
|
%%% @end |
5 |
|
%%%============================================================================= |
6 |
|
-module(cyrsasl_scram). |
7 |
|
|
8 |
|
-export([mech_new/3, mech_step/2]). |
9 |
|
|
10 |
|
-include("mongoose.hrl"). |
11 |
|
-include("jlib.hrl"). |
12 |
|
|
13 |
|
-behaviour(cyrsasl). |
14 |
|
|
15 |
|
-type sha() :: sha | sha224 | sha256 | sha384 | sha512. |
16 |
|
-type scram_att() :: {module(), scram_keys()}. |
17 |
|
-type scram_keys() :: term(). |
18 |
|
-type error() :: {error, binary()} | {error, binary(), binary()}. |
19 |
|
|
20 |
|
-define(SALT_LENGTH, 16). |
21 |
|
-define(NONCE_LENGTH, 16). |
22 |
|
|
23 |
|
mech_new(LServer, Creds, #{sha := Sha, |
24 |
|
socket := Socket, |
25 |
|
auth_mech := AuthMech, |
26 |
|
scram_plus := ScramPlus}) -> |
27 |
55 |
ChannelBinding = calculate_channel_binding(Socket, ScramPlus, Sha, AuthMech), |
28 |
55 |
{ok, St0} = fast_scram:mech_new( |
29 |
|
#{entity => server, |
30 |
|
hash_method => Sha, |
31 |
|
retrieve_mechanism => retrieve_mechanism_fun(LServer, Creds, Sha), |
32 |
|
nonce_size => ?NONCE_LENGTH, |
33 |
|
channel_binding => ChannelBinding}), |
34 |
55 |
St1 = fast_scram:mech_set(creds, Creds, St0), |
35 |
55 |
{ok, St1}. |
36 |
|
|
37 |
|
retrieve_mechanism_fun(LServer, Creds, Sha) -> |
38 |
55 |
fun(Username, St0) -> |
39 |
50 |
case jid:make_bare(Username, LServer) of |
40 |
1 |
error -> {error, {invalid_username, Username}}; |
41 |
|
JID -> |
42 |
49 |
retrieve_mechanism_continue(JID, Creds, Sha, St0) |
43 |
|
end |
44 |
|
end. |
45 |
|
|
46 |
|
retrieve_mechanism_continue(#jid{luser = Username} = JID, Creds, Sha, St0) -> |
47 |
49 |
HostType = mongoose_credentials:host_type(Creds), |
48 |
49 |
case get_scram_attributes(HostType, JID, Sha) of |
49 |
|
{AuthModule, {StoredKey, ServerKey, Salt, ItCount}} -> |
50 |
48 |
Creds1 = fast_scram:mech_get(creds, St0, Creds), |
51 |
48 |
R = [{username, Username}, {auth_module, AuthModule}], |
52 |
48 |
Creds2 = mongoose_credentials:extend(Creds1, R), |
53 |
48 |
St1 = fast_scram:mech_set(creds, Creds2, St0), |
54 |
48 |
ExtraConfig = #{it_count => ItCount, salt => Salt, |
55 |
|
auth_data => #{stored_key => StoredKey, |
56 |
|
server_key => ServerKey}}, |
57 |
48 |
{St1, ExtraConfig}; |
58 |
|
{error, Reason, User} -> |
59 |
1 |
{error, {Reason, User}} |
60 |
|
end. |
61 |
|
|
62 |
|
mech_step(State, ClientIn) -> |
63 |
100 |
case fast_scram:mech_step(State, ClientIn) of |
64 |
|
{continue, Msg, NewState} -> |
65 |
48 |
{continue, Msg, NewState}; |
66 |
|
{ok, Msg, FinalState} -> |
67 |
42 |
Creds0 = fast_scram:mech_get(creds, FinalState), |
68 |
42 |
R = [{sasl_success_response, Msg}], |
69 |
42 |
Creds1 = mongoose_credentials:extend(Creds0, R), |
70 |
42 |
{ok, Creds1}; |
71 |
|
{error, Reason, _} -> |
72 |
10 |
?LOG_INFO(#{what => scram_authentication_failed, reason => Reason}), |
73 |
10 |
{error, <<"not-authorized">>} |
74 |
|
end. |
75 |
|
|
76 |
|
-spec get_scram_attributes(mongooseim:host_type(), jid:jid(), sha()) -> scram_att() | error(). |
77 |
|
get_scram_attributes(HostType, JID, Sha) -> |
78 |
49 |
case ejabberd_auth:get_passterm_with_authmodule(HostType, JID) of |
79 |
|
false -> |
80 |
1 |
{UserName, _} = jid:to_lus(JID), |
81 |
1 |
{error, <<"not-authorized">>, UserName}; |
82 |
|
{Params, AuthModule} -> |
83 |
48 |
{AuthModule, do_get_scram_attributes(Params, Sha)} |
84 |
|
end. |
85 |
|
|
86 |
|
do_get_scram_attributes(#{iteration_count := IterationCount} = Params, Sha) -> |
87 |
37 |
#{Sha := #{salt := Salt, stored_key := StoredKey, server_key := ServerKey}} = Params, |
88 |
37 |
{base64:decode(StoredKey), base64:decode(ServerKey), |
89 |
|
base64:decode(Salt), IterationCount}; |
90 |
|
do_get_scram_attributes(Password, Sha) -> |
91 |
11 |
TempSalt = crypto:strong_rand_bytes(?SALT_LENGTH), |
92 |
11 |
SaltedPassword = mongoose_scram:salted_password(Sha, Password, TempSalt, |
93 |
|
mongoose_scram:iterations()), |
94 |
11 |
{fast_scram:stored_key(Sha, fast_scram:client_key(Sha, SaltedPassword)), |
95 |
|
fast_scram:server_key(Sha, SaltedPassword), TempSalt, mongoose_scram:iterations()}. |
96 |
|
|
97 |
|
%%-------------------------------------------------------------------- |
98 |
|
%% Helpers |
99 |
|
%%-------------------------------------------------------------------- |
100 |
|
calculate_channel_binding(Socket, ScramPlus, Sha, AuthMech) -> |
101 |
55 |
{CBVariant, CBData} = maybe_get_tls_last_message(Socket, ScramPlus), |
102 |
55 |
Advertised = is_scram_plus_advertised(Sha, AuthMech), |
103 |
55 |
case {Advertised, CBVariant} of |
104 |
35 |
{true, _} -> {CBVariant, CBData}; |
105 |
20 |
{false, none} -> {undefined, <<>>}; |
106 |
:-( |
{false, CBVariant} -> {CBVariant, CBData} |
107 |
|
end. |
108 |
|
|
109 |
|
maybe_get_tls_last_message(Socket, true) -> |
110 |
20 |
case mongoose_c2s_socket:get_tls_last_message(Socket) of |
111 |
|
{error, _Error} -> |
112 |
:-( |
{none, <<>>}; |
113 |
|
{ok, Msg} -> |
114 |
20 |
{<<"tls-unique">>, Msg} |
115 |
|
end; |
116 |
|
maybe_get_tls_last_message(_, _) -> |
117 |
35 |
{none, <<>>}. |
118 |
|
|
119 |
15 |
is_scram_plus_advertised(sha, Mech) -> lists:member(<<"SCRAM-SHA-1-PLUS">>, Mech); |
120 |
9 |
is_scram_plus_advertised(sha224, Mech) -> lists:member(<<"SCRAM-SHA-224-PLUS">>, Mech); |
121 |
13 |
is_scram_plus_advertised(sha256, Mech) -> lists:member(<<"SCRAM-SHA-256-PLUS">>, Mech); |
122 |
9 |
is_scram_plus_advertised(sha384, Mech) -> lists:member(<<"SCRAM-SHA-384-PLUS">>, Mech); |
123 |
9 |
is_scram_plus_advertised(sha512, Mech) -> lists:member(<<"SCRAM-SHA-512-PLUS">>, Mech). |