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_authentication
|
||||||
, emqx_web_hook
|
, emqx_web_hook
|
||||||
, emqx_rule_engine
|
, emqx_rule_engine
|
||||||
, emqx_sasl
|
|
||||||
, emqx_statsd
|
, emqx_statsd
|
||||||
]
|
]
|
||||||
++ relx_plugin_apps_per_rel(ReleaseType)
|
++ relx_plugin_apps_per_rel(ReleaseType)
|
||||||
|
|
Loading…
Reference in New Issue