Merge branch 'master' of https://github.com/emqx/emqx into merge-4.3-to-5.0
This commit is contained in:
commit
d6e531bd8f
|
@ -1,26 +0,0 @@
|
|||
.eunit
|
||||
deps
|
||||
*.o
|
||||
*.beam
|
||||
*.plt
|
||||
erl_crash.dump
|
||||
ebin
|
||||
rel/example_project
|
||||
.concrete/DEV_MODE
|
||||
.rebar
|
||||
data/
|
||||
*.swp
|
||||
*.swo
|
||||
.erlang.mk/
|
||||
emqx_sasl.d
|
||||
erlang.mk
|
||||
rebar3.crashdump
|
||||
_build
|
||||
cover/
|
||||
ct.coverdata
|
||||
eunit.coverdata
|
||||
logs/
|
||||
rebar.lock
|
||||
test/ct.cover.spec
|
||||
etc/emqx_sasl.conf.rendered
|
||||
.rebar3/
|
|
@ -1,2 +0,0 @@
|
|||
# emqx-sasl
|
||||
Simple Authentication and Security Layer
|
|
@ -1,19 +0,0 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-define(APP, emqx_sasl).
|
||||
|
||||
-define(SCRAM_AUTH_TAB, scram_auth).
|
|
@ -1,19 +0,0 @@
|
|||
{deps,
|
||||
[{pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {branch, "2.0.4"}}}
|
||||
]}.
|
||||
|
||||
{edoc_opts, [{preprocess, true}]}.
|
||||
{erl_opts, [warn_unused_vars,
|
||||
warn_shadow_vars,
|
||||
warnings_as_errors,
|
||||
warn_unused_import,
|
||||
warn_obsolete_guard,
|
||||
debug_info,
|
||||
{parse_transform}]}.
|
||||
|
||||
{xref_checks, [undefined_function_calls, undefined_functions,
|
||||
locals_not_used, deprecated_function_calls,
|
||||
warnings_as_errors, deprecated_functions]}.
|
||||
{cover_enabled, true}.
|
||||
{cover_opts, [verbose]}.
|
||||
{cover_export_enabled, true}.
|
|
@ -1,227 +0,0 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_sasl_api).
|
||||
|
||||
-include("emqx_sasl.hrl").
|
||||
|
||||
-import(minirest, [ return/0
|
||||
, return/1
|
||||
]).
|
||||
|
||||
-rest_api(#{name => add,
|
||||
method => 'POST',
|
||||
path => "/sasl",
|
||||
func => add,
|
||||
descr => "Add authentication information"}).
|
||||
|
||||
-rest_api(#{name => delete,
|
||||
method => 'DELETE',
|
||||
path => "/sasl",
|
||||
func => delete,
|
||||
descr => "Delete authentication information"}).
|
||||
|
||||
-rest_api(#{name => update,
|
||||
method => 'PUT',
|
||||
path => "/sasl",
|
||||
func => update,
|
||||
descr => "Update authentication information"}).
|
||||
|
||||
-rest_api(#{name => get,
|
||||
method => 'GET',
|
||||
path => "/sasl",
|
||||
func => get,
|
||||
descr => "Get authentication information"}).
|
||||
|
||||
-export([ add/2
|
||||
, delete/2
|
||||
, update/2
|
||||
, get/2
|
||||
]).
|
||||
|
||||
add(_Bindings, Params) ->
|
||||
case pipeline([fun ensure_required_add_params/1,
|
||||
fun validate_params/1,
|
||||
fun do_add/1], Params) of
|
||||
ok ->
|
||||
return();
|
||||
{error, Reason} ->
|
||||
return({error, Reason})
|
||||
end.
|
||||
|
||||
delete(_Bindings, Params) ->
|
||||
case pipeline([fun ensure_required_delete_params/1,
|
||||
fun validate_params/1,
|
||||
fun do_delete/1], Params) of
|
||||
ok ->
|
||||
return();
|
||||
{error, Reason} ->
|
||||
return({error, Reason})
|
||||
end.
|
||||
|
||||
update(_Bindings, Params) ->
|
||||
case pipeline([fun ensure_required_add_params/1,
|
||||
fun validate_params/1,
|
||||
fun do_update/1], Params) of
|
||||
ok ->
|
||||
return();
|
||||
{error, Reason} ->
|
||||
return({error, Reason})
|
||||
end.
|
||||
|
||||
get(Bindings, Params) when is_list(Params) ->
|
||||
get(Bindings, maps:from_list(Params));
|
||||
|
||||
get(_Bindings, #{<<"mechanism">> := Mechanism0,
|
||||
<<"username">> := Username0}) ->
|
||||
Mechanism = urldecode(Mechanism0),
|
||||
Username = urldecode(Username0),
|
||||
case Mechanism of
|
||||
<<"SCRAM-SHA-1">> ->
|
||||
case emqx_sasl_scram:lookup(Username) of
|
||||
{ok, AuthInfo = #{salt := Salt}} ->
|
||||
return({ok, AuthInfo#{salt => base64:decode(Salt)}});
|
||||
{error, Reason} ->
|
||||
return({error, Reason})
|
||||
end;
|
||||
_ ->
|
||||
return({error, unsupported_mechanism})
|
||||
end;
|
||||
get(_Bindings, #{<<"mechanism">> := Mechanism}) ->
|
||||
case urldecode(Mechanism) of
|
||||
<<"SCRAM-SHA-1">> ->
|
||||
Data = #{Mechanism => mnesia:dirty_all_keys(?SCRAM_AUTH_TAB)},
|
||||
return({ok, Data});
|
||||
_ ->
|
||||
return({error, <<"Unsupported mechanism">>})
|
||||
end;
|
||||
|
||||
get(_Bindings, _Params) ->
|
||||
Data = lists:foldl(fun(Mechanism, Acc) ->
|
||||
case Mechanism of
|
||||
<<"SCRAM-SHA-1">> ->
|
||||
[#{Mechanism => mnesia:dirty_all_keys(?SCRAM_AUTH_TAB)} | Acc]
|
||||
end
|
||||
end, [], emqx_sasl:supported()),
|
||||
return({ok, Data}).
|
||||
|
||||
ensure_required_add_params(Params) when is_list(Params) ->
|
||||
case proplists:get_value(<<"mechanism">>, Params) of
|
||||
undefined ->
|
||||
{missing, missing_required_param};
|
||||
Mechaism ->
|
||||
ensure_required_add_params(Mechaism, Params)
|
||||
end.
|
||||
|
||||
ensure_required_add_params(<<"SCRAM-SHA-1">>, Params) ->
|
||||
Required = [<<"username">>, <<"password">>, <<"salt">>],
|
||||
case erlang:map_size(maps:with(Required, maps:from_list(Params))) =:= erlang:length(Required) of
|
||||
true -> ok;
|
||||
false -> {missing, missing_required_param}
|
||||
end;
|
||||
ensure_required_add_params(_, _) ->
|
||||
{error, unsupported_mechanism}.
|
||||
|
||||
ensure_required_delete_params(Params) when is_list(Params) ->
|
||||
case proplists:get_value(<<"mechanism">>, Params) of
|
||||
undefined ->
|
||||
{missing, missing_required_param};
|
||||
Mechaism ->
|
||||
ensure_required_delete_params(Mechaism, Params)
|
||||
end.
|
||||
|
||||
ensure_required_delete_params(<<"SCRAM-SHA-1">>, Params) ->
|
||||
Required = [<<"username">>],
|
||||
case erlang:map_size(maps:with(Required, maps:from_list(Params))) =:= erlang:length(Required) of
|
||||
true -> ok;
|
||||
false -> {missing, missing_required_param}
|
||||
end;
|
||||
ensure_required_delete_params(_, _) ->
|
||||
{error, unsupported_mechanism}.
|
||||
|
||||
validate_params(Params) ->
|
||||
Mechaism = proplists:get_value(<<"mechanism">>, Params),
|
||||
validate_params(Mechaism, Params).
|
||||
|
||||
validate_params(<<"SCRAM-SHA-1">>, []) ->
|
||||
ok;
|
||||
validate_params(<<"SCRAM-SHA-1">>, [{<<"username">>, Username} | More]) when is_binary(Username) ->
|
||||
validate_params(<<"SCRAM-SHA-1">>, More);
|
||||
validate_params(<<"SCRAM-SHA-1">>, [{<<"username">>, _} | _]) ->
|
||||
{error, invalid_username};
|
||||
validate_params(<<"SCRAM-SHA-1">>, [{<<"password">>, Password} | More]) when is_binary(Password) ->
|
||||
validate_params(<<"SCRAM-SHA-1">>, More);
|
||||
validate_params(<<"SCRAM-SHA-1">>, [{<<"password">>, _} | _]) ->
|
||||
{error, invalid_password};
|
||||
validate_params(<<"SCRAM-SHA-1">>, [{<<"salt">>, Salt} | More]) when is_binary(Salt) ->
|
||||
validate_params(<<"SCRAM-SHA-1">>, More);
|
||||
validate_params(<<"SCRAM-SHA-1">>, [{<<"salt">>, _} | _]) ->
|
||||
{error, invalid_salt};
|
||||
validate_params(<<"SCRAM-SHA-1">>, [{<<"iteration_count">>, IterationCount} | More]) when is_integer(IterationCount) ->
|
||||
validate_params(<<"SCRAM-SHA-1">>, More);
|
||||
validate_params(<<"SCRAM-SHA-1">>, [{<<"iteration_count">>, _} | _]) ->
|
||||
{error, invalid_iteration_count};
|
||||
validate_params(<<"SCRAM-SHA-1">>, [_ | More]) ->
|
||||
validate_params(<<"SCRAM-SHA-1">>, More).
|
||||
|
||||
do_add(Params) ->
|
||||
Mechaism = proplists:get_value(<<"mechanism">>, Params),
|
||||
do_add(Mechaism, Params).
|
||||
|
||||
do_add(<<"SCRAM-SHA-1">>, Params) ->
|
||||
Username = proplists:get_value(<<"username">>, Params),
|
||||
Password = proplists:get_value(<<"password">>, Params),
|
||||
Salt = proplists:get_value(<<"salt">>, Params),
|
||||
IterationCount = proplists:get_value(<<"iteration_count">>, Params, 4096),
|
||||
emqx_sasl_scram:add(Username, Password, Salt, IterationCount);
|
||||
do_add(_, _) ->
|
||||
{error, unsupported_mechanism}.
|
||||
|
||||
do_delete(Params) ->
|
||||
Mechaism = proplists:get_value(<<"mechanism">>, Params),
|
||||
do_delete(Mechaism, Params).
|
||||
|
||||
do_delete(<<"SCRAM-SHA-1">>, Params) ->
|
||||
Username = proplists:get_value(<<"username">>, Params),
|
||||
emqx_sasl_scram:delete(Username);
|
||||
do_delete(_, _) ->
|
||||
{error, unsupported_mechanism}.
|
||||
|
||||
do_update(Params) ->
|
||||
Mechaism = proplists:get_value(<<"mechanism">>, Params),
|
||||
do_update(Mechaism, Params).
|
||||
|
||||
do_update(<<"SCRAM-SHA-1">>, Params) ->
|
||||
Username = proplists:get_value(<<"username">>, Params),
|
||||
Password = proplists:get_value(<<"password">>, Params),
|
||||
Salt = proplists:get_value(<<"salt">>, Params),
|
||||
IterationCount = proplists:get_value(<<"iteration_count">>, Params, 4096),
|
||||
emqx_sasl_scram:update(Username, Password, Salt, IterationCount);
|
||||
do_update(_, _) ->
|
||||
{error, unsupported_mechanism}.
|
||||
|
||||
pipeline([], _) ->
|
||||
ok;
|
||||
pipeline([Fun | More], Params) ->
|
||||
case Fun(Params) of
|
||||
ok ->
|
||||
pipeline(More, Params);
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
urldecode(S) ->
|
||||
emqx_http_lib:uri_decode(S).
|
|
@ -1,46 +0,0 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_sasl_app).
|
||||
|
||||
-behaviour(application).
|
||||
|
||||
-emqx_plugin(?MODULE).
|
||||
|
||||
-export([ start/2
|
||||
, stop/1
|
||||
]).
|
||||
|
||||
-behaviour(supervisor).
|
||||
|
||||
-export([init/1]).
|
||||
|
||||
start(_Type, _Args) ->
|
||||
ok = emqx_sasl:init(),
|
||||
_ = emqx_sasl:load(),
|
||||
emqx_sasl_cli:load(),
|
||||
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
||||
|
||||
stop(_State) ->
|
||||
emqx_sasl_cli:unload(),
|
||||
emqx_sasl:unload().
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Dummy supervisor
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
init([]) ->
|
||||
{ok, { {one_for_all, 1, 10}, []} }.
|
|
@ -1,82 +0,0 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_sasl_cli).
|
||||
|
||||
-include("emqx_sasl.hrl").
|
||||
|
||||
%% APIs
|
||||
-export([ load/0
|
||||
, unload/0
|
||||
, cli/1
|
||||
]).
|
||||
|
||||
load() ->
|
||||
emqx_ctl:register_command(sasl, {?MODULE, cli}, []).
|
||||
|
||||
unload() ->
|
||||
emqx_ctl:unregister_command(sasl).
|
||||
|
||||
cli(["scram", "add", Username, Password, Salt]) ->
|
||||
cli(["scram", "add", Username, Password, Salt, "4096"]);
|
||||
cli(["scram", "add", Username, Password, Salt, IterationCount]) ->
|
||||
case emqx_sasl_scram:add(list_to_binary(Username),
|
||||
list_to_binary(Password),
|
||||
list_to_binary(Salt),
|
||||
list_to_integer(IterationCount)) of
|
||||
ok ->
|
||||
emqx_ctl:print("Authentication information added successfully~n");
|
||||
{error, already_existed} ->
|
||||
emqx_ctl:print("Authentication information already exists~n")
|
||||
end;
|
||||
|
||||
cli(["scram", "delete", Username0]) ->
|
||||
Username = list_to_binary(Username0),
|
||||
ok = emqx_sasl_scram:delete(Username),
|
||||
emqx_ctl:print("Authentication information deleted successfully~n");
|
||||
|
||||
cli(["scram", "update", Username, Password, Salt]) ->
|
||||
cli(["scram", "update", Username, Password, Salt, "4096"]);
|
||||
cli(["scram", "update", Username, Password, Salt, IterationCount]) ->
|
||||
case emqx_sasl_scram:update(list_to_binary(Username),
|
||||
list_to_binary(Password),
|
||||
list_to_binary(Salt),
|
||||
list_to_integer(IterationCount)) of
|
||||
ok ->
|
||||
emqx_ctl:print("Authentication information updated successfully~n");
|
||||
{error, not_found} ->
|
||||
emqx_ctl:print("Authentication information not found~n")
|
||||
end;
|
||||
|
||||
cli(["scram", "lookup", Username0]) ->
|
||||
Username = list_to_binary(Username0),
|
||||
case emqx_sasl_scram:lookup(Username) of
|
||||
{ok, #{username := Username,
|
||||
stored_key := StoredKey,
|
||||
server_key := ServerKey,
|
||||
salt := Salt,
|
||||
iteration_count := IterationCount}} ->
|
||||
emqx_ctl:print("Username: ~s, Stored Key: ~s, Server Key: ~s, Salt: ~s, Iteration Count: ~p~n",
|
||||
[Username, StoredKey, ServerKey, base64:decode(Salt), IterationCount]);
|
||||
{error, not_found} ->
|
||||
emqx_ctl:print("Authentication information not found~n")
|
||||
end;
|
||||
|
||||
cli(_) ->
|
||||
emqx_ctl:usage([{"sasl scram add <Username> <Password> <Salt> [<IterationCount>]", "Add SCRAM-SHA-1 authentication information"},
|
||||
{"sasl scram delete <Username>", "Delete SCRAM-SHA-1 authentication information"},
|
||||
{"sasl scram update <Username> <Password> <Salt> [<IterationCount>]", "Update SCRAM-SHA-1 authentication information"},
|
||||
{"sasl scram lookup <Username>", "Check if SCRAM-SHA-1 authentication information exists"}]).
|
|
@ -1,310 +0,0 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_sasl_scram).
|
||||
|
||||
-include("emqx_sasl.hrl").
|
||||
|
||||
-export([ init/0
|
||||
, add/3
|
||||
, add/4
|
||||
, update/3
|
||||
, update/4
|
||||
, delete/1
|
||||
, lookup/1
|
||||
, check/2
|
||||
, make_client_first/1]).
|
||||
|
||||
-record(?SCRAM_AUTH_TAB, {
|
||||
username,
|
||||
stored_key,
|
||||
server_key,
|
||||
salt,
|
||||
iteration_count :: integer()
|
||||
}).
|
||||
|
||||
-ifdef(TEST).
|
||||
-compile(export_all).
|
||||
-compile(nowarn_export_all).
|
||||
-endif.
|
||||
|
||||
init() ->
|
||||
ok = ekka_mnesia:create_table(?SCRAM_AUTH_TAB, [
|
||||
{disc_copies, [node()]},
|
||||
{attributes, record_info(fields, ?SCRAM_AUTH_TAB)},
|
||||
{storage_properties, [{ets, [{read_concurrency, true}]}]}]),
|
||||
ok = ekka_mnesia:copy_table(?SCRAM_AUTH_TAB, disc_copies).
|
||||
|
||||
add(Username, Password, Salt) ->
|
||||
add(Username, Password, Salt, 4096).
|
||||
|
||||
add(Username, Password, Salt, IterationCount) ->
|
||||
case lookup(Username) of
|
||||
{error, not_found} ->
|
||||
do_add(Username, Password, Salt, IterationCount);
|
||||
_ ->
|
||||
{error, already_existed}
|
||||
end.
|
||||
|
||||
update(Username, Password, Salt) ->
|
||||
update(Username, Password, Salt, 4096).
|
||||
|
||||
update(Username, Password, Salt, IterationCount) ->
|
||||
case lookup(Username) of
|
||||
{error, not_found} ->
|
||||
{error, not_found};
|
||||
_ ->
|
||||
do_add(Username, Password, Salt, IterationCount)
|
||||
end.
|
||||
|
||||
delete(Username) ->
|
||||
ret(mnesia:transaction(fun mnesia:delete/3, [?SCRAM_AUTH_TAB, Username, write])).
|
||||
|
||||
lookup(Username) ->
|
||||
case mnesia:dirty_read(?SCRAM_AUTH_TAB, Username) of
|
||||
[#scram_auth{username = Username,
|
||||
stored_key = StoredKey,
|
||||
server_key = ServerKey,
|
||||
salt = Salt,
|
||||
iteration_count = IterationCount}] ->
|
||||
{ok, #{username => Username,
|
||||
stored_key => StoredKey,
|
||||
server_key => ServerKey,
|
||||
salt => Salt,
|
||||
iteration_count => IterationCount}};
|
||||
[] ->
|
||||
{error, not_found}
|
||||
end.
|
||||
|
||||
do_add(Username, Password, Salt, IterationCount) ->
|
||||
SaltedPassword = pbkdf2_sha_1(Password, Salt, IterationCount),
|
||||
ClientKey = client_key(SaltedPassword),
|
||||
ServerKey = server_key(SaltedPassword),
|
||||
StoredKey = crypto:hash(sha, ClientKey),
|
||||
AuthInfo = #scram_auth{username = Username,
|
||||
stored_key = base64:encode(StoredKey),
|
||||
server_key = base64:encode(ServerKey),
|
||||
salt = base64:encode(Salt),
|
||||
iteration_count = IterationCount},
|
||||
ret(mnesia:transaction(fun mnesia:write/3, [?SCRAM_AUTH_TAB, AuthInfo, write])).
|
||||
|
||||
ret({atomic, ok}) -> ok;
|
||||
ret({aborted, Error}) -> {error, Error}.
|
||||
|
||||
check(Data, Cache) when map_size(Cache) =:= 0 ->
|
||||
check_client_first(Data);
|
||||
check(Data, Cache) ->
|
||||
case maps:get(next_step, Cache, undefined) of
|
||||
undefined -> check_server_first(Data, Cache);
|
||||
check_client_final -> check_client_final(Data, Cache);
|
||||
check_server_final -> check_server_final(Data, Cache)
|
||||
end.
|
||||
|
||||
check_client_first(ClientFirst) ->
|
||||
ClientFirstWithoutHeader = without_header(ClientFirst),
|
||||
Attributes = parse(ClientFirstWithoutHeader),
|
||||
Username = proplists:get_value(username, Attributes),
|
||||
ClientNonce = proplists:get_value(nonce, Attributes),
|
||||
case lookup(Username) of
|
||||
{error, not_found} ->
|
||||
{error, not_found};
|
||||
{ok, #{stored_key := StoredKey0,
|
||||
server_key := ServerKey0,
|
||||
salt := Salt0,
|
||||
iteration_count := IterationCount}} ->
|
||||
StoredKey = base64:decode(StoredKey0),
|
||||
ServerKey = base64:decode(ServerKey0),
|
||||
Salt = base64:decode(Salt0),
|
||||
ServerNonce = nonce(),
|
||||
Nonce = list_to_binary(binary_to_list(ClientNonce) ++ binary_to_list(ServerNonce)),
|
||||
ServerFirst = make_server_first(Nonce, Salt, IterationCount),
|
||||
{continue, ServerFirst, #{next_step => check_client_final,
|
||||
client_first_without_header => ClientFirstWithoutHeader,
|
||||
server_first => ServerFirst,
|
||||
stored_key => StoredKey,
|
||||
server_key => ServerKey,
|
||||
nonce => Nonce}}
|
||||
end.
|
||||
|
||||
check_client_final(ClientFinal, #{client_first_without_header := ClientFirstWithoutHeader,
|
||||
server_first := ServerFirst,
|
||||
server_key := ServerKey,
|
||||
stored_key := StoredKey,
|
||||
nonce := OldNonce}) ->
|
||||
ClientFinalWithoutProof = without_proof(ClientFinal),
|
||||
Attributes = parse(ClientFinal),
|
||||
ClientProof = base64:decode(proplists:get_value(proof, Attributes)),
|
||||
NewNonce = proplists:get_value(nonce, Attributes),
|
||||
Auth0 = io_lib:format("~s,~s,~s", [ClientFirstWithoutHeader, ServerFirst, ClientFinalWithoutProof]),
|
||||
Auth = iolist_to_binary(Auth0),
|
||||
ClientSignature = hmac(StoredKey, Auth),
|
||||
ClientKey = crypto:exor(ClientProof, ClientSignature),
|
||||
case NewNonce =:= OldNonce andalso crypto:hash(sha, ClientKey) =:= StoredKey of
|
||||
true ->
|
||||
ServerSignature = hmac(ServerKey, Auth),
|
||||
ServerFinal = make_server_final(ServerSignature),
|
||||
{ok, ServerFinal, #{}};
|
||||
false ->
|
||||
{error, invalid_client_final}
|
||||
end.
|
||||
|
||||
check_server_first(ServerFirst, #{password := Password,
|
||||
client_first := ClientFirst}) ->
|
||||
Attributes = parse(ServerFirst),
|
||||
Nonce = proplists:get_value(nonce, Attributes),
|
||||
ClientFirstWithoutHeader = without_header(ClientFirst),
|
||||
ClientFinalWithoutProof = serialize([{channel_binding, <<"biws">>}, {nonce, Nonce}]),
|
||||
Auth = list_to_binary(io_lib:format("~s,~s,~s", [ClientFirstWithoutHeader, ServerFirst, ClientFinalWithoutProof])),
|
||||
Salt = base64:decode(proplists:get_value(salt, Attributes)),
|
||||
IterationCount = binary_to_integer(proplists:get_value(iteration_count, Attributes)),
|
||||
SaltedPassword = pbkdf2_sha_1(Password, Salt, IterationCount),
|
||||
ClientKey = client_key(SaltedPassword),
|
||||
StoredKey = crypto:hash(sha, ClientKey),
|
||||
ClientSignature = hmac(StoredKey, Auth),
|
||||
ClientProof = base64:encode(crypto:exor(ClientKey, ClientSignature)),
|
||||
ClientFinal = serialize([{channel_binding, <<"biws">>},
|
||||
{nonce, Nonce},
|
||||
{proof, ClientProof}]),
|
||||
{continue, ClientFinal, #{next_step => check_server_final,
|
||||
password => Password,
|
||||
client_first => ClientFirst,
|
||||
server_first => ServerFirst}}.
|
||||
|
||||
check_server_final(ServerFinal, #{password := Password,
|
||||
client_first := ClientFirst,
|
||||
server_first := ServerFirst}) ->
|
||||
NewAttributes = parse(ServerFinal),
|
||||
Attributes = parse(ServerFirst),
|
||||
Nonce = proplists:get_value(nonce, Attributes),
|
||||
ClientFirstWithoutHeader = without_header(ClientFirst),
|
||||
ClientFinalWithoutProof = serialize([{channel_binding, <<"biws">>}, {nonce, Nonce}]),
|
||||
Auth = list_to_binary(io_lib:format("~s,~s,~s", [ClientFirstWithoutHeader, ServerFirst, ClientFinalWithoutProof])),
|
||||
Salt = base64:decode(proplists:get_value(salt, Attributes)),
|
||||
IterationCount = binary_to_integer(proplists:get_value(iteration_count, Attributes)),
|
||||
SaltedPassword = pbkdf2_sha_1(Password, Salt, IterationCount),
|
||||
ServerKey = server_key(SaltedPassword),
|
||||
ServerSignature = hmac(ServerKey, Auth),
|
||||
case base64:encode(ServerSignature) =:= proplists:get_value(verifier, NewAttributes) of
|
||||
true ->
|
||||
{ok, <<>>, #{}};
|
||||
false ->
|
||||
{stop, invalid_server_final}
|
||||
end.
|
||||
|
||||
make_client_first(Username) ->
|
||||
list_to_binary("n,," ++ binary_to_list(serialize([{username, Username}, {nonce, nonce()}]))).
|
||||
|
||||
make_server_first(Nonce, Salt, IterationCount) ->
|
||||
serialize([{nonce, Nonce}, {salt, base64:encode(Salt)}, {iteration_count, IterationCount}]).
|
||||
|
||||
make_server_final(ServerSignature) ->
|
||||
serialize([{verifier, base64:encode(ServerSignature)}]).
|
||||
|
||||
nonce() ->
|
||||
base64:encode([$a + rand:uniform(26) || _ <- lists:seq(1, 10)]).
|
||||
|
||||
pbkdf2_sha_1(Password, Salt, IterationCount) ->
|
||||
{ok, Bin} = pbkdf2:pbkdf2(sha, Password, Salt, IterationCount),
|
||||
pbkdf2:to_hex(Bin).
|
||||
|
||||
-if(?OTP_RELEASE >= 23).
|
||||
hmac(Key, Data) ->
|
||||
HMAC = crypto:mac_init(hmac, sha, Key),
|
||||
HMAC1 = crypto:mac_update(HMAC, Data),
|
||||
crypto:mac_final(HMAC1).
|
||||
-else.
|
||||
hmac(Key, Data) ->
|
||||
HMAC = crypto:hmac_init(sha, Key),
|
||||
HMAC1 = crypto:hmac_update(HMAC, Data),
|
||||
crypto:hmac_final(HMAC1).
|
||||
-endif.
|
||||
|
||||
client_key(SaltedPassword) ->
|
||||
hmac(<<"Client Key">>, SaltedPassword).
|
||||
|
||||
server_key(SaltedPassword) ->
|
||||
hmac(<<"Server Key">>, SaltedPassword).
|
||||
|
||||
without_header(<<"n,,", ClientFirstWithoutHeader/binary>>) ->
|
||||
ClientFirstWithoutHeader;
|
||||
without_header(<<GS2CbindFlag:1/binary, _/binary>>) ->
|
||||
error({unsupported_gs2_cbind_flag, binary_to_atom(GS2CbindFlag, utf8)}).
|
||||
|
||||
without_proof(ClientFinal) ->
|
||||
[ClientFinalWithoutProof | _] = binary:split(ClientFinal, <<",p=">>, [global, trim_all]),
|
||||
ClientFinalWithoutProof.
|
||||
|
||||
parse(Message) ->
|
||||
Attributes = binary:split(Message, <<$,>>, [global, trim_all]),
|
||||
lists:foldl(fun(<<Key:1/binary, "=", Value/binary>>, Acc) ->
|
||||
[{to_long(Key), Value} | Acc]
|
||||
end, [], Attributes).
|
||||
|
||||
serialize(Attributes) ->
|
||||
iolist_to_binary(
|
||||
lists:foldl(fun({Key, Value}, []) ->
|
||||
[to_short(Key), "=", to_list(Value)];
|
||||
({Key, Value}, Acc) ->
|
||||
Acc ++ [",", to_short(Key), "=", to_list(Value)]
|
||||
end, [], Attributes)).
|
||||
|
||||
to_long(<<"a">>) ->
|
||||
authzid;
|
||||
to_long(<<"c">>) ->
|
||||
channel_binding;
|
||||
to_long(<<"n">>) ->
|
||||
username;
|
||||
to_long(<<"p">>) ->
|
||||
proof;
|
||||
to_long(<<"r">>) ->
|
||||
nonce;
|
||||
to_long(<<"s">>) ->
|
||||
salt;
|
||||
to_long(<<"v">>) ->
|
||||
verifier;
|
||||
to_long(<<"i">>) ->
|
||||
iteration_count;
|
||||
to_long(_) ->
|
||||
error(test).
|
||||
|
||||
to_short(authzid) ->
|
||||
"a";
|
||||
to_short(channel_binding) ->
|
||||
"c";
|
||||
to_short(username) ->
|
||||
"n";
|
||||
to_short(proof) ->
|
||||
"p";
|
||||
to_short(nonce) ->
|
||||
"r";
|
||||
to_short(salt) ->
|
||||
"s";
|
||||
to_short(verifier) ->
|
||||
"v";
|
||||
to_short(iteration_count) ->
|
||||
"i";
|
||||
to_short(_) ->
|
||||
error(test).
|
||||
|
||||
to_list(V) when is_binary(V) ->
|
||||
binary_to_list(V);
|
||||
to_list(V) when is_list(V) ->
|
||||
V;
|
||||
to_list(V) when is_integer(V) ->
|
||||
integer_to_list(V);
|
||||
to_list(_) ->
|
||||
error(bad_type).
|
||||
|
|
@ -1,140 +0,0 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_sasl_scram_SUITE).
|
||||
|
||||
-compile(export_all).
|
||||
-compile(nowarn_export_all).
|
||||
|
||||
-include_lib("common_test/include/ct.hrl").
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
init_per_suite(Config) ->
|
||||
emqx_ct_helpers:start_apps([emqx_sasl]),
|
||||
Config.
|
||||
|
||||
end_per_suite(_Config) ->
|
||||
emqx_ct_helpers:stop_apps([]).
|
||||
|
||||
all() -> emqx_ct:all(?MODULE).
|
||||
|
||||
t_crud(_) ->
|
||||
Username = <<"test">>,
|
||||
Password = <<"public">>,
|
||||
Salt = <<"emqx">>,
|
||||
IterationCount = 4096,
|
||||
EncodedSalt = base64:encode(Salt),
|
||||
SaltedPassword = emqx_sasl_scram:pbkdf2_sha_1(Password, Salt, IterationCount),
|
||||
ClientKey = emqx_sasl_scram:client_key(SaltedPassword),
|
||||
ServerKey = base64:encode(emqx_sasl_scram:server_key(SaltedPassword)),
|
||||
StoredKey = base64:encode(crypto:hash(sha, ClientKey)),
|
||||
|
||||
{error, not_found} = emqx_sasl_scram:lookup(Username),
|
||||
ok = emqx_sasl_scram:add(Username, Password, Salt),
|
||||
{error, already_existed} = emqx_sasl_scram:add(Username, Password, Salt),
|
||||
|
||||
{ok, #{username := Username,
|
||||
stored_key := StoredKey,
|
||||
server_key := ServerKey,
|
||||
salt := EncodedSalt,
|
||||
iteration_count := IterationCount}} = emqx_sasl_scram:lookup(Username),
|
||||
|
||||
NewSalt = <<"new salt">>,
|
||||
NewEncodedSalt = base64:encode(NewSalt),
|
||||
emqx_sasl_scram:update(Username, Password, NewSalt),
|
||||
{ok, #{username := Username,
|
||||
salt := NewEncodedSalt}} = emqx_sasl_scram:lookup(Username),
|
||||
emqx_sasl_scram:delete(Username),
|
||||
{error, not_found} = emqx_sasl_scram:lookup(Username).
|
||||
|
||||
t_scram(_) ->
|
||||
AuthMethod = <<"SCRAM-SHA-1">>,
|
||||
[AuthMethod] = emqx_sasl:supported(),
|
||||
|
||||
Username = <<"test">>,
|
||||
Password = <<"public">>,
|
||||
Salt = <<"emqx">>,
|
||||
ok = emqx_sasl_scram:add(Username, Password, Salt),
|
||||
ClientFirst = emqx_sasl_scram:make_client_first(Username),
|
||||
|
||||
{ok, {continue, ServerFirst, Cache}} = emqx_sasl:check(AuthMethod, ClientFirst, #{}),
|
||||
|
||||
{ok, {continue, ClientFinal, ClientCache}} = emqx_sasl:check(AuthMethod, ServerFirst, #{password => Password, client_first => ClientFirst}),
|
||||
|
||||
{ok, {ok, ServerFinal, #{}}} = emqx_sasl:check(AuthMethod, ClientFinal, Cache),
|
||||
|
||||
{ok, _} = emqx_sasl:check(AuthMethod, ServerFinal, ClientCache).
|
||||
|
||||
%t_proto(_) ->
|
||||
% process_flag(trap_exit, true),
|
||||
%
|
||||
% Username = <<"username">>,
|
||||
% Password = <<"password">>,
|
||||
% Salt = <<"emqx">>,
|
||||
% AuthMethod = <<"SCRAM-SHA-1">>,
|
||||
%
|
||||
% {ok, Client0} = emqtt:start_link([{clean_start, true},
|
||||
% {proto_ver, v5},
|
||||
% {enhanced_auth, #{method => AuthMethod,
|
||||
% params => #{username => Username,
|
||||
% password => Password,
|
||||
% salt => Salt}}},
|
||||
% {connect_timeout, 6000}]),
|
||||
% {error,{not_authorized,#{}}} = emqtt:connect(Client0),
|
||||
%
|
||||
% ok = emqx_sasl_scram:add(Username, Password, Salt),
|
||||
% {ok, Client1} = emqtt:start_link([{clean_start, true},
|
||||
% {proto_ver, v5},
|
||||
% {enhanced_auth, #{method => AuthMethod,
|
||||
% params => #{username => Username,
|
||||
% password => Password,
|
||||
% salt => Salt}}},
|
||||
% {connect_timeout, 6000}]),
|
||||
% {ok, _} = emqtt:connect(Client1),
|
||||
%
|
||||
% timer:sleep(200),
|
||||
% ok = emqtt:reauthentication(Client1, #{params => #{username => Username,
|
||||
% password => Password,
|
||||
% salt => Salt}}),
|
||||
%
|
||||
% timer:sleep(200),
|
||||
% ErrorFun = fun (_State) -> {ok, <<>>, #{}} end,
|
||||
% ok = emqtt:reauthentication(Client1, #{params => #{},function => ErrorFun}),
|
||||
% receive
|
||||
% {disconnected,ReasonCode2,#{}} ->
|
||||
% ?assertEqual(ReasonCode2, 135)
|
||||
% after 500 ->
|
||||
% error("emqx re-authentication failed")
|
||||
% end,
|
||||
%
|
||||
% {ok, Client2} = emqtt:start_link([{clean_start, true},
|
||||
% {proto_ver, v5},
|
||||
% {enhanced_auth, #{method => AuthMethod,
|
||||
% params => #{},
|
||||
% function =>fun (_State) -> {ok, <<>>, #{}} end}},
|
||||
% {connect_timeout, 6000}]),
|
||||
% {error,{not_authorized,#{}}} = emqtt:connect(Client2),
|
||||
%
|
||||
% receive_msg(),
|
||||
% process_flag(trap_exit, false).
|
||||
|
||||
receive_msg() ->
|
||||
receive
|
||||
{'EXIT', Msg} ->
|
||||
ct:print("==========+~p~n", [Msg]),
|
||||
receive_msg()
|
||||
after 200 -> ok
|
||||
end.
|
|
@ -287,7 +287,6 @@ relx_plugin_apps(ReleaseType) ->
|
|||
, emqx_authentication
|
||||
, emqx_web_hook
|
||||
, emqx_rule_engine
|
||||
, emqx_sasl
|
||||
, emqx_statsd
|
||||
]
|
||||
++ relx_plugin_apps_per_rel(ReleaseType)
|
||||
|
|
Loading…
Reference in New Issue