./ct_report/coverage/cyrsasl_scram.COVER.html

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).
Line Hits Source