Merge pull request #6384 from savonarola/refactor-password-hashing

refactor(authn): unify password hashing
This commit is contained in:
Ilya Averyanov 2021-12-14 16:21:02 +03:00 committed by GitHub
commit b8a68d7a9f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 680 additions and 371 deletions

View File

@ -250,7 +250,7 @@ set_peercert_infos(Peercert, ClientInfo, Zone) ->
dn -> DN;
crt -> Peercert;
pem when is_binary(Peercert) -> base64:encode(Peercert);
md5 when is_binary(Peercert) -> emqx_passwd:hash(md5, Peercert);
md5 when is_binary(Peercert) -> emqx_passwd:hash_data(md5, Peercert);
_ -> undefined
end
end,

View File

@ -17,81 +17,120 @@
-module(emqx_passwd).
-export([ hash/2
, check_pass/2
, hash_data/2
, check_pass/3
]).
-export_type([ password/0
, password_hash/0
, hash_type_simple/0
, hash_type/0
, salt_position/0
, salt/0]).
-include("logger.hrl").
-type(hash_type() :: plain | md5 | sha | sha256 | sha512 | pbkdf2 | bcrypt).
-type(password() :: binary()).
-type(password_hash() :: binary()).
-export_type([hash_type/0]).
-type(hash_type_simple() :: plain | md5 | sha | sha256 | sha512).
-type(hash_type() :: hash_type_simple() | bcrypt | pbkdf2).
-type(salt_position() :: prefix | suffix).
-type(salt() :: binary()).
-type(pbkdf2_mac_fun() :: md4 | md5 | ripemd160 | sha | sha224 | sha256 | sha384 | sha512).
-type(pbkdf2_iterations() :: pos_integer()).
-type(pbkdf2_dk_length() :: pos_integer() | undefined).
-type(hash_params() ::
{bcrypt, salt()} |
{pbkdf2, pbkdf2_mac_fun(), salt(), pbkdf2_iterations(), pbkdf2_dk_length()} |
{hash_type_simple(), salt(), salt_position()}).
-export_type([pbkdf2_mac_fun/0]).
%%--------------------------------------------------------------------
%% APIs
%%--------------------------------------------------------------------
-spec(check_pass(binary() | tuple(), binary() | tuple())
-> ok | {error, term()}).
check_pass({PassHash, Password}, bcrypt) ->
try
Salt = binary:part(PassHash, {0, 29}),
check_pass(PassHash, emqx_passwd:hash(bcrypt, {Salt, Password}))
catch
error:badarg -> {error, incorrect_hash}
-spec(check_pass(hash_params(), password_hash(), password()) -> boolean()).
check_pass({pbkdf2, MacFun, Salt, Iterations, DKLength}, PasswordHash, Password) ->
case pbkdf2(MacFun, Password, Salt, Iterations, DKLength) of
{ok, HashPasswd} ->
compare_secure(hex(HashPasswd), PasswordHash);
{error, _Reason}->
false
end;
check_pass({PassHash, Password}, HashType) ->
check_pass(PassHash, emqx_passwd:hash(HashType, Password));
check_pass({PassHash, Salt, Password}, {pbkdf2, Macfun, Iterations, Dklen}) ->
check_pass(PassHash, emqx_passwd:hash(pbkdf2, {Salt, Password, Macfun, Iterations, Dklen}));
check_pass({PassHash, Salt, Password}, {salt, bcrypt}) ->
check_pass(PassHash, emqx_passwd:hash(bcrypt, {Salt, Password}));
check_pass({PassHash, Salt, Password}, {bcrypt, salt}) ->
check_pass(PassHash, emqx_passwd:hash(bcrypt, {Salt, Password}));
check_pass({PassHash, Salt, Password}, {salt, HashType}) ->
check_pass(PassHash, emqx_passwd:hash(HashType, <<Salt/binary, Password/binary>>));
check_pass({PassHash, Salt, Password}, {HashType, salt}) ->
check_pass(PassHash, emqx_passwd:hash(HashType, <<Password/binary, Salt/binary>>));
check_pass(PassHash, PassHash) -> ok;
check_pass(_Hash1, _Hash2) -> {error, password_error}.
check_pass({bcrypt, Salt}, PasswordHash, Password) ->
case bcrypt:hashpw(Password, Salt) of
{ok, HashPasswd} ->
compare_secure(list_to_binary(HashPasswd), PasswordHash);
{error, _Reason}->
false
end;
check_pass({_SimpleHash, _Salt, _SaltPosition} = HashParams, PasswordHash, Password) ->
Hash = hash(HashParams, Password),
compare_secure(Hash, PasswordHash).
-spec(hash(hash_type(), binary() | tuple()) -> binary()).
hash(plain, Password) ->
Password;
hash(md5, Password) ->
hexstring(crypto:hash(md5, Password));
hash(sha, Password) ->
hexstring(crypto:hash(sha, Password));
hash(sha256, Password) ->
hexstring(crypto:hash(sha256, Password));
hash(sha512, Password) ->
hexstring(crypto:hash(sha512, Password));
hash(pbkdf2, {Salt, Password, Macfun, Iterations, Dklen}) ->
case pbkdf2:pbkdf2(Macfun, Password, Salt, Iterations, Dklen) of
{ok, Hexstring} ->
pbkdf2:to_hex(Hexstring);
{error, Reason} ->
?SLOG(error, #{msg => "pbkdf2_hash_error", reason => Reason}),
<<>>
-spec(hash(hash_params(), password()) -> password_hash()).
hash({pbkdf2, MacFun, Salt, Iterations, DKLength}, Password) ->
case pbkdf2(MacFun, Password, Salt, Iterations, DKLength) of
{ok, HashPasswd} ->
hex(HashPasswd);
{error, Reason}->
error(Reason)
end;
hash(bcrypt, {Salt, Password}) ->
{ok, _} = application:ensure_all_started(bcrypt),
hash({bcrypt, Salt}, Password) ->
case bcrypt:hashpw(Password, Salt) of
{ok, HashPasswd} ->
list_to_binary(HashPasswd);
{error, Reason}->
?SLOG(error, #{msg => "bcrypt_hash_error", reason => Reason}),
<<>>
error(Reason)
end;
hash({SimpleHash, Salt, prefix}, Password) when is_binary(Password), is_binary(Salt) ->
hash_data(SimpleHash, <<Salt/binary, Password/binary>>);
hash({SimpleHash, Salt, suffix}, Password) when is_binary(Password), is_binary(Salt) ->
hash_data(SimpleHash, <<Password/binary, Salt/binary>>).
-spec(hash_data(hash_type(), binary()) -> binary()).
hash_data(plain, Data) when is_binary(Data) ->
Data;
hash_data(md5, Data) when is_binary(Data) ->
hex(crypto:hash(md5, Data));
hash_data(sha, Data) when is_binary(Data) ->
hex(crypto:hash(sha, Data));
hash_data(sha256, Data) when is_binary(Data) ->
hex(crypto:hash(sha256, Data));
hash_data(sha512, Data) when is_binary(Data) ->
hex(crypto:hash(sha512, Data)).
%%--------------------------------------------------------------------
%% Internal functions
%%--------------------------------------------------------------------
compare_secure(X, Y) when is_binary(X), is_binary(Y) ->
compare_secure(binary_to_list(X), binary_to_list(Y));
compare_secure(X, Y) when is_list(X), is_list(Y) ->
case length(X) == length(Y) of
true ->
compare_secure(X, Y, 0);
false ->
false
end.
%%--------------------------------------------------------------------
%% Internal funcs
%%--------------------------------------------------------------------
compare_secure([X | RestX], [Y | RestY], Result) ->
compare_secure(RestX, RestY, (X bxor Y) bor Result);
compare_secure([], [], Result) ->
Result == 0.
hexstring(<<X:128/big-unsigned-integer>>) ->
iolist_to_binary(io_lib:format("~32.16.0b", [X]));
hexstring(<<X:160/big-unsigned-integer>>) ->
iolist_to_binary(io_lib:format("~40.16.0b", [X]));
hexstring(<<X:256/big-unsigned-integer>>) ->
iolist_to_binary(io_lib:format("~64.16.0b", [X]));
hexstring(<<X:512/big-unsigned-integer>>) ->
iolist_to_binary(io_lib:format("~128.16.0b", [X])).
pbkdf2(MacFun, Password, Salt, Iterations, undefined) ->
pbkdf2:pbkdf2(MacFun, Password, Salt, Iterations);
pbkdf2(MacFun, Password, Salt, Iterations, DKLength) ->
pbkdf2:pbkdf2(MacFun, Password, Salt, Iterations, DKLength).
hex(X) when is_binary(X) ->
pbkdf2:to_hex(X).

View File

@ -19,13 +19,85 @@
-compile(nowarn_export_all).
-compile(export_all).
all() -> [t_hash].
-include_lib("eunit/include/eunit.hrl").
all() ->
emqx_common_test_helpers:all(?MODULE).
groups() ->
[].
init_per_suite(Config) ->
{ok, _} = application:ensure_all_started(bcrypt),
Config.
end_per_suite(_Config) ->
ok.
t_hash_data(_) ->
Password = <<"password">>,
Password = emqx_passwd:hash_data(plain, Password),
<<"5f4dcc3b5aa765d61d8327deb882cf99">>
= emqx_passwd:hash_data(md5, Password),
<<"5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8">>
= emqx_passwd:hash_data(sha, Password),
<<"5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8">>
= emqx_passwd:hash_data(sha256, Password),
Sha512 = iolist_to_binary(
[<<"b109f3bbbc244eb82441917ed06d618b9008dd09b3befd1b5e07394c706a8bb9">>,
<<"80b1d7785e5976ec049b46df5f1326af5a2ea6d103fd07c95385ffab0cacbc86">>]),
Sha512 = emqx_passwd:hash_data(sha512, Password).
t_hash(_) ->
Password = <<"password">>, Salt = <<"salt">>,
_ = emqx_passwd:hash(plain, Password),
_ = emqx_passwd:hash(md5, Password),
_ = emqx_passwd:hash(sha, Password),
_ = emqx_passwd:hash(sha256, Password),
_ = emqx_passwd:hash(bcrypt, {Salt, Password}),
_ = emqx_passwd:hash(pbkdf2, {Salt, Password, sha256, 1000, 20}).
Password = <<"password">>,
Salt = <<"salt">>,
WrongPassword = <<"wrongpass">>,
Md5 = <<"67a1e09bb1f83f5007dc119c14d663aa">>,
Md5 = emqx_passwd:hash({md5, Salt, prefix}, Password),
true = emqx_passwd:check_pass({md5, Salt, prefix}, Md5, Password),
false = emqx_passwd:check_pass({md5, Salt, prefix}, Md5, WrongPassword),
Sha = <<"59b3e8d637cf97edbe2384cf59cb7453dfe30789">>,
Sha = emqx_passwd:hash({sha, Salt, prefix}, Password),
true = emqx_passwd:check_pass({sha, Salt, prefix}, Sha, Password),
false = emqx_passwd:check_pass({sha, Salt, prefix}, Sha, WrongPassword),
Sha256 = <<"7a37b85c8918eac19a9089c0fa5a2ab4dce3f90528dcdeec108b23ddf3607b99">>,
Sha256 = emqx_passwd:hash({sha256, Salt, suffix}, Password),
true = emqx_passwd:check_pass({sha256, Salt, suffix}, Sha256, Password),
false = emqx_passwd:check_pass({sha256, Salt, suffix}, Sha256, WrongPassword),
Sha512 = iolist_to_binary(
[<<"fa6a2185b3e0a9a85ef41ffb67ef3c1fb6f74980f8ebf970e4e72e353ed9537d">>,
<<"593083c201dfd6e43e1c8a7aac2bc8dbb119c7dfb7d4b8f131111395bd70e97f">>]),
Sha512 = emqx_passwd:hash({sha512, Salt, suffix}, Password),
true = emqx_passwd:check_pass({sha512, Salt, suffix}, Sha512, Password),
false = emqx_passwd:check_pass({sha512, Salt, suffix}, Sha512, WrongPassword),
BcryptSalt = <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>,
Bcrypt = <<"$2b$12$wtY3h20mUjjmeaClpqZVvehyw7F.V78F3rbK2xDkCzRTMi6pmfUB6">>,
Bcrypt = emqx_passwd:hash({bcrypt, BcryptSalt}, Password),
true = emqx_passwd:check_pass({bcrypt, Bcrypt}, Bcrypt, Password),
false = emqx_passwd:check_pass({bcrypt, Bcrypt}, Bcrypt, WrongPassword),
false = emqx_passwd:check_pass({bcrypt, <<>>}, <<>>, WrongPassword),
%% Invalid salt, bcrypt fails
?assertException(error, _, emqx_passwd:hash({bcrypt, Salt}, Password)),
BadDKlen = 1 bsl 32,
Pbkdf2Salt = <<"ATHENA.MIT.EDUraeburn">>,
Pbkdf2 = <<"01dbee7f4a9e243e988b62c73cda935d"
"a05378b93244ec8f48a99e61ad799d86">>,
Pbkdf2 = emqx_passwd:hash({pbkdf2, sha, Pbkdf2Salt, 2, 32}, Password),
true = emqx_passwd:check_pass({pbkdf2, sha, Pbkdf2Salt, 2, 32}, Pbkdf2, Password),
false = emqx_passwd:check_pass({pbkdf2, sha, Pbkdf2Salt, 2, 32}, Pbkdf2, WrongPassword),
false = emqx_passwd:check_pass({pbkdf2, sha, Pbkdf2Salt, 2, BadDKlen}, Pbkdf2, Password),
%% Invalid derived_length, pbkdf2 fails
?assertException(error, _, emqx_passwd:hash({pbkdf2, sha, Pbkdf2Salt, 2, BadDKlen}, Password)).

View File

@ -0,0 +1,167 @@
%%--------------------------------------------------------------------
%% Copyright (c) 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_authn_password_hashing).
-include_lib("typerefl/include/types.hrl").
-type(simple_algorithm_name() :: plain | md5 | sha | sha256 | sha512).
-type(salt_position() :: prefix | suffix).
-type(simple_algorithm() :: #{name := simple_algorithm_name(),
salt_position := salt_position()}).
-type(bcrypt_algorithm() :: #{name := bcrypt}).
-type(bcrypt_algorithm_rw() :: #{name := bcrypt, salt_rounds := integer()}).
-type(pbkdf2_algorithm() :: #{name := pbkdf2,
mac_fun := emqx_passwd:pbkdf2_mac_fun(),
iterations := pos_integer()}).
-type(algorithm() :: simple_algorithm() | pbkdf2_algorithm() | bcrypt_algorithm()).
-type(algorithm_rw() :: simple_algorithm() | pbkdf2_algorithm() | bcrypt_algorithm_rw()).
%%------------------------------------------------------------------------------
%% Hocon Schema
%%------------------------------------------------------------------------------
-behaviour(hocon_schema).
-export([roots/0,
fields/1]).
-export([type_ro/1,
type_rw/1]).
-export([init/1,
gen_salt/1,
hash/2,
check_password/4]).
roots() -> [pbkdf2, bcrypt, bcrypt_rw, other_algorithms].
fields(bcrypt_rw) ->
fields(bcrypt) ++
[{salt_rounds, fun salt_rounds/1}];
fields(bcrypt) ->
[{name, {enum, [bcrypt]}}];
fields(pbkdf2) ->
[{name, {enum, [pbkdf2]}},
{mac_fun, {enum, [md4, md5, ripemd160, sha, sha224, sha256, sha384, sha512]}},
{iterations, integer()},
{dk_length, fun dk_length/1}];
fields(other_algorithms) ->
[{name, {enum, [plain, md5, sha, sha256, sha512]}},
{salt_position, fun salt_position/1}].
salt_position(type) -> {enum, [prefix, suffix]};
salt_position(default) -> prefix;
salt_position(_) -> undefined.
salt_rounds(type) -> integer();
salt_rounds(default) -> 10;
salt_rounds(_) -> undefined.
dk_length(type) -> integer();
dk_length(nullable) -> true;
dk_length(default) -> undefined;
dk_length(_) -> undefined.
type_rw(type) ->
hoconsc:union(rw_refs());
type_rw(default) -> #{<<"name">> => sha256, <<"salt_position">> => prefix};
type_rw(_) -> undefined.
type_ro(type) ->
hoconsc:union(ro_refs());
type_ro(default) -> #{<<"name">> => sha256, <<"salt_position">> => prefix};
type_ro(_) -> undefined.
%%------------------------------------------------------------------------------
%% APIs
%%------------------------------------------------------------------------------
-spec(init(algorithm()) -> ok).
init(#{name := bcrypt}) ->
{ok, _} = application:ensure_all_started(bcrypt),
ok;
init(#{name := _Other}) ->
ok.
-spec(gen_salt(algorithm_rw()) -> emqx_passwd:salt()).
gen_salt(#{name := plain}) ->
<<>>;
gen_salt(#{name := bcrypt,
salt_rounds := Rounds}) ->
{ok, Salt} = bcrypt:gen_salt(Rounds),
list_to_binary(Salt);
gen_salt(#{name := Other}) when Other =/= plain, Other =/= bcrypt ->
<<X:128/big-unsigned-integer>> = crypto:strong_rand_bytes(16),
iolist_to_binary(io_lib:format("~32.16.0b", [X])).
-spec(hash(algorithm_rw(), emqx_passwd:password()) -> {emqx_passwd:hash(), emqx_passwd:salt()}).
hash(#{name := bcrypt, salt_rounds := _} = Algorithm, Password) ->
Salt0 = gen_salt(Algorithm),
Hash = emqx_passwd:hash({bcrypt, Salt0}, Password),
Salt = Hash,
{Hash, Salt};
hash(#{name := pbkdf2,
mac_fun := MacFun,
iterations := Iterations} = Algorithm, Password) ->
Salt = gen_salt(Algorithm),
DKLength = maps:get(dk_length, Algorithm, undefined),
Hash = emqx_passwd:hash({pbkdf2, MacFun, Salt, Iterations, DKLength}, Password),
{Hash, Salt};
hash(#{name := Other, salt_position := SaltPosition} = Algorithm, Password) ->
Salt = gen_salt(Algorithm),
Hash = emqx_passwd:hash({Other, Salt, SaltPosition}, Password),
{Hash, Salt}.
-spec(check_password(
algorithm(),
emqx_passwd:salt(),
emqx_passwd:hash(),
emqx_passwd:password()) -> boolean()).
check_password(#{name := bcrypt}, _Salt, PasswordHash, Password) ->
emqx_passwd:check_pass({bcrypt, PasswordHash}, PasswordHash, Password);
check_password(#{name := pbkdf2,
mac_fun := MacFun,
iterations := Iterations} = Algorithm,
Salt, PasswordHash, Password) ->
DKLength = maps:get(dk_length, Algorithm, undefined),
emqx_passwd:check_pass({pbkdf2, MacFun, Salt, Iterations, DKLength}, PasswordHash, Password);
check_password(#{name := Other, salt_position := SaltPosition}, Salt, PasswordHash, Password) ->
emqx_passwd:check_pass({Other, Salt, SaltPosition}, PasswordHash, Password).
%%------------------------------------------------------------------------------
%% Internal functions
%%------------------------------------------------------------------------------
rw_refs() ->
[hoconsc:ref(?MODULE, bcrypt_rw),
hoconsc:ref(?MODULE, pbkdf2),
hoconsc:ref(?MODULE, other_algorithms)].
ro_refs() ->
[hoconsc:ref(?MODULE, bcrypt),
hoconsc:ref(?MODULE, pbkdf2),
hoconsc:ref(?MODULE, other_algorithms)].

View File

@ -18,12 +18,10 @@
-include_lib("emqx/include/emqx_placeholder.hrl").
-export([ replace_placeholders/2
-export([ check_password_from_selected_map/3
, replace_placeholders/2
, replace_placeholder/2
, check_password/3
, is_superuser/1
, hash/4
, gen_salt/0
, bin/1
, ensure_apps_started/1
, cleanup_resources/0
@ -36,6 +34,17 @@
%% APIs
%%------------------------------------------------------------------------------
check_password_from_selected_map(_Algorithm, _Selected, undefined) ->
{error, bad_username_or_password};
check_password_from_selected_map(
Algorithm, #{<<"password_hash">> := Hash} = Selected, Password) ->
Salt = maps:get(<<"salt">>, Selected, <<>>),
case emqx_authn_password_hashing:check_password(Algorithm, Salt, Hash, Password) of
true -> ok;
false ->
{error, bad_username_or_password}
end.
replace_placeholders(PlaceHolders, Data) ->
replace_placeholders(PlaceHolders, Data, []).
@ -64,27 +73,6 @@ replace_placeholder(?PH_CERT_CN_NAME, Credential) ->
replace_placeholder(Constant, _) ->
Constant.
check_password(undefined, _Selected, _State) ->
{error, bad_username_or_password};
check_password(Password,
#{<<"password_hash">> := Hash},
#{password_hash_algorithm := bcrypt}) ->
case emqx_passwd:hash(bcrypt, {Hash, Password}) of
Hash -> ok;
_ ->
{error, bad_username_or_password}
end;
check_password(Password,
#{<<"password_hash">> := Hash} = Selected,
#{password_hash_algorithm := Algorithm,
salt_position := SaltPosition}) ->
Salt = maps:get(<<"salt">>, Selected, <<>>),
case hash(Algorithm, Password, Salt, SaltPosition) of
Hash -> ok;
_ ->
{error, bad_username_or_password}
end.
is_superuser(#{<<"is_superuser">> := <<"">>}) ->
#{is_superuser => false};
is_superuser(#{<<"is_superuser">> := <<"0">>}) ->
@ -108,15 +96,6 @@ ensure_apps_started(bcrypt) ->
ensure_apps_started(_) ->
ok.
hash(Algorithm, Password, Salt, prefix) ->
emqx_passwd:hash(Algorithm, <<Salt/binary, Password/binary>>);
hash(Algorithm, Password, Salt, suffix) ->
emqx_passwd:hash(Algorithm, <<Password/binary, Salt/binary>>).
gen_salt() ->
<<X:128/big-unsigned-integer>> = crypto:strong_rand_bytes(16),
iolist_to_binary(io_lib:format("~32.16.0b", [X])).
bin(A) when is_atom(A) -> atom_to_binary(A, utf8);
bin(L) when is_list(L) -> list_to_binary(L);
bin(X) -> X.

View File

@ -91,31 +91,13 @@ fields(?CONF_NS) ->
[ {mechanism, emqx_authn_schema:mechanism('password-based')}
, {backend, emqx_authn_schema:backend('built-in-database')}
, {user_id_type, fun user_id_type/1}
, {password_hash_algorithm, fun password_hash_algorithm/1}
] ++ emqx_authn_schema:common_fields();
fields(bcrypt) ->
[ {name, {enum, [bcrypt]}}
, {salt_rounds, fun salt_rounds/1}
];
fields(other_algorithms) ->
[ {name, {enum, [plain, md5, sha, sha256, sha512]}}
].
, {password_hash_algorithm, fun emqx_authn_password_hashing:type_rw/1}
] ++ emqx_authn_schema:common_fields().
user_id_type(type) -> user_id_type();
user_id_type(default) -> <<"username">>;
user_id_type(_) -> undefined.
password_hash_algorithm(type) -> hoconsc:union([hoconsc:ref(?MODULE, bcrypt),
hoconsc:ref(?MODULE, other_algorithms)]);
password_hash_algorithm(default) -> #{<<"name">> => sha256};
password_hash_algorithm(_) -> undefined.
salt_rounds(type) -> integer();
salt_rounds(default) -> 10;
salt_rounds(_) -> undefined.
%%------------------------------------------------------------------------------
%% APIs
%%------------------------------------------------------------------------------
@ -125,22 +107,11 @@ refs() ->
create(AuthenticatorID,
#{user_id_type := Type,
password_hash_algorithm := #{name := bcrypt,
salt_rounds := SaltRounds}}) ->
ok = emqx_authn_utils:ensure_apps_started(bcrypt),
password_hash_algorithm := Algorithm}) ->
ok = emqx_authn_password_hashing:init(Algorithm),
State = #{user_group => AuthenticatorID,
user_id_type => Type,
password_hash_algorithm => bcrypt,
salt_rounds => SaltRounds},
{ok, State};
create(AuthenticatorID,
#{user_id_type := Type,
password_hash_algorithm := #{name := Name}}) ->
ok = emqx_authn_utils:ensure_apps_started(Name),
State = #{user_group => AuthenticatorID,
user_id_type => Type,
password_hash_algorithm => Name},
password_hash_algorithm => Algorithm},
{ok, State}.
update(Config, #{user_group := ID}) ->
@ -156,12 +127,9 @@ authenticate(#{password := Password} = Credential,
case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of
[] ->
ignore;
[#user_info{password_hash = PasswordHash, salt = Salt0, is_superuser = IsSuperuser}] ->
Salt = case Algorithm of
bcrypt -> PasswordHash;
_ -> Salt0
end,
case PasswordHash =:= hash(Algorithm, Password, Salt) of
[#user_info{password_hash = PasswordHash, salt = Salt, is_superuser = IsSuperuser}] ->
case emqx_authn_password_hashing:check_password(
Algorithm, Salt, PasswordHash, Password) of
true -> {ok, #{is_superuser => IsSuperuser}};
false -> {error, bad_username_or_password}
end
@ -193,12 +161,13 @@ import_users(Filename0, State) ->
add_user(#{user_id := UserID,
password := Password} = UserInfo,
#{user_group := UserGroup} = State) ->
#{user_group := UserGroup,
password_hash_algorithm := Algorithm}) ->
trans(
fun() ->
case mnesia:read(?TAB, {UserGroup, UserID}, write) of
[] ->
{PasswordHash, Salt} = hash(Password, State),
{PasswordHash, Salt} = emqx_authn_password_hashing:hash(Algorithm, Password),
IsSuperuser = maps:get(is_superuser, UserInfo, false),
insert_user(UserGroup, UserID, PasswordHash, Salt, IsSuperuser),
{ok, #{user_id => UserID, is_superuser => IsSuperuser}};
@ -219,7 +188,8 @@ delete_user(UserID, #{user_group := UserGroup}) ->
end).
update_user(UserID, UserInfo,
#{user_group := UserGroup} = State) ->
#{user_group := UserGroup,
password_hash_algorithm := Algorithm}) ->
trans(
fun() ->
case mnesia:read(?TAB, {UserGroup, UserID}, write) of
@ -229,11 +199,12 @@ update_user(UserID, UserInfo,
, salt = Salt
, is_superuser = IsSuperuser}] ->
NSuperuser = maps:get(is_superuser, UserInfo, IsSuperuser),
{NPasswordHash, NSalt} = case maps:get(password, UserInfo, undefined) of
undefined ->
{PasswordHash, Salt};
Password ->
hash(Password, State)
{NPasswordHash, NSalt} = case UserInfo of
#{password := Password} ->
emqx_authn_password_hashing:hash(
Algorithm, Password);
#{} ->
{PasswordHash, Salt}
end,
insert_user(UserGroup, UserID, NPasswordHash, NSalt, NSuperuser),
{ok, #{user_id => UserID, is_superuser => NSuperuser}}
@ -349,26 +320,6 @@ get_user_info_by_seq([<<"false">> | More1], [<<"is_superuser">> | More2], Acc) -
get_user_info_by_seq(_, _, _) ->
{error, bad_format}.
gen_salt(#{password_hash_algorithm := plain}) ->
<<>>;
gen_salt(#{password_hash_algorithm := bcrypt,
salt_rounds := Rounds}) ->
{ok, Salt} = bcrypt:gen_salt(Rounds),
Salt;
gen_salt(_) ->
emqx_authn_utils:gen_salt().
hash(bcrypt, Password, Salt) ->
{ok, Hash} = bcrypt:hashpw(Password, Salt),
list_to_binary(Hash);
hash(Algorithm, Password, Salt) ->
emqx_passwd:hash(Algorithm, <<Salt/binary, Password/binary>>).
hash(Password, #{password_hash_algorithm := Algorithm} = State) ->
Salt = gen_salt(State),
PasswordHash = hash(Algorithm, Password, Salt),
{PasswordHash, Salt}.
insert_user(UserGroup, UserID, PasswordHash, Salt, IsSuperuser) ->
UserInfo = #user_info{user_id = {UserGroup, UserID},
password_hash = PasswordHash,

View File

@ -63,8 +63,7 @@ common_fields() ->
, {password_hash_field, fun password_hash_field/1}
, {salt_field, fun salt_field/1}
, {is_superuser_field, fun is_superuser_field/1}
, {password_hash_algorithm, fun password_hash_algorithm/1}
, {salt_position, fun salt_position/1}
, {password_hash_algorithm, fun emqx_authn_password_hashing:type_ro/1}
] ++ emqx_authn_schema:common_fields().
collection(type) -> binary();
@ -84,14 +83,6 @@ is_superuser_field(type) -> binary();
is_superuser_field(nullable) -> true;
is_superuser_field(_) -> undefined.
password_hash_algorithm(type) -> {enum, [plain, md5, sha, sha256, sha512, bcrypt]};
password_hash_algorithm(default) -> sha256;
password_hash_algorithm(_) -> undefined.
salt_position(type) -> {enum, [prefix, suffix]};
salt_position(default) -> prefix;
salt_position(_) -> undefined.
%%------------------------------------------------------------------------------
%% APIs
%%------------------------------------------------------------------------------
@ -116,7 +107,7 @@ create(#{selector := Selector} = Config) ->
salt_position],
Config),
#{password_hash_algorithm := Algorithm} = State,
ok = emqx_authn_utils:ensure_apps_started(Algorithm),
ok = emqx_authn_password_hashing:init(Algorithm),
ResourceId = emqx_authn_utils:make_resource_id(?MODULE),
NState = State#{
selector => NSelector,
@ -203,24 +194,10 @@ normalize_selector(Selector) ->
check_password(undefined, _Selected, _State) ->
{error, bad_username_or_password};
check_password(Password,
Doc,
#{password_hash_algorithm := bcrypt,
password_hash_field := PasswordHashField}) ->
case maps:get(PasswordHashField, Doc, undefined) of
undefined ->
{error, {cannot_find_password_hash_field, PasswordHashField}};
Hash ->
case {ok, to_list(Hash)} =:= bcrypt:hashpw(Password, Hash) of
true -> ok;
false -> {error, bad_username_or_password}
end
end;
check_password(Password,
Doc,
#{password_hash_algorithm := Algorithm,
password_hash_field := PasswordHashField,
salt_position := SaltPosition} = State) ->
password_hash_field := PasswordHashField} = State) ->
case maps:get(PasswordHashField, Doc, undefined) of
undefined ->
{error, {cannot_find_password_hash_field, PasswordHashField}};
@ -229,7 +206,7 @@ check_password(Password,
undefined -> <<>>;
SaltField -> maps:get(SaltField, Doc, <<>>)
end,
case Hash =:= hash(Algorithm, Password, Salt, SaltPosition) of
case emqx_authn_password_hashing:check_password(Algorithm, Salt, Hash, Password) of
true -> ok;
false -> {error, bad_username_or_password}
end
@ -240,12 +217,3 @@ is_superuser(Doc, #{is_superuser_field := IsSuperuserField}) ->
emqx_authn_utils:is_superuser(#{<<"is_superuser">> => IsSuperuser});
is_superuser(_, _) ->
emqx_authn_utils:is_superuser(#{<<"is_superuser">> => false}).
hash(Algorithm, Password, Salt, prefix) ->
emqx_passwd:hash(Algorithm, <<Salt/binary, Password/binary>>);
hash(Algorithm, Password, Salt, suffix) ->
emqx_passwd:hash(Algorithm, <<Password/binary, Salt/binary>>).
to_list(L) when is_list(L) -> L;
to_list(L) when is_binary(L) -> binary_to_list(L);
to_list(X) -> X.

View File

@ -46,22 +46,13 @@ roots() -> [?CONF_NS].
fields(?CONF_NS) ->
[ {mechanism, emqx_authn_schema:mechanism('password-based')}
, {backend, emqx_authn_schema:backend(mysql)}
, {password_hash_algorithm, fun password_hash_algorithm/1}
, {salt_position, fun salt_position/1}
, {password_hash_algorithm, fun emqx_authn_password_hashing:type_ro/1}
, {query, fun query/1}
, {query_timeout, fun query_timeout/1}
] ++ emqx_authn_schema:common_fields()
++ emqx_connector_schema_lib:relational_db_fields()
++ emqx_connector_schema_lib:ssl_fields().
password_hash_algorithm(type) -> {enum, [plain, md5, sha, sha256, sha512, bcrypt]};
password_hash_algorithm(default) -> sha256;
password_hash_algorithm(_) -> undefined.
salt_position(type) -> {enum, [prefix, suffix]};
salt_position(default) -> prefix;
salt_position(_) -> undefined.
query(type) -> string();
query(_) -> undefined.
@ -80,14 +71,13 @@ create(_AuthenticatorID, Config) ->
create(Config).
create(#{password_hash_algorithm := Algorithm,
salt_position := SaltPosition,
query := Query0,
query_timeout := QueryTimeout
} = Config) ->
ok = emqx_authn_password_hashing:init(Algorithm),
{Query, PlaceHolders} = parse_query(Query0),
ResourceId = emqx_authn_utils:make_resource_id(?MODULE),
State = #{password_hash_algorithm => Algorithm,
salt_position => SaltPosition,
query => Query,
placeholders => PlaceHolders,
query_timeout => QueryTimeout,
@ -116,13 +106,15 @@ authenticate(#{password := Password} = Credential,
#{placeholders := PlaceHolders,
query := Query,
query_timeout := Timeout,
resource_id := ResourceId} = State) ->
resource_id := ResourceId,
password_hash_algorithm := Algorithm}) ->
Params = emqx_authn_utils:replace_placeholders(PlaceHolders, Credential),
case emqx_resource:query(ResourceId, {sql, Query, Params, Timeout}) of
{ok, _Columns, []} -> ignore;
{ok, Columns, [Row | _]} ->
Selected = maps:from_list(lists:zip(Columns, Row)),
case emqx_authn_utils:check_password(Password, Selected, State) of
case emqx_authn_utils:check_password_from_selected_map(
Algorithm, Selected, Password) of
ok ->
{ok, emqx_authn_utils:is_superuser(Selected)};
{error, Reason} ->

View File

@ -52,21 +52,12 @@ roots() -> [?CONF_NS].
fields(?CONF_NS) ->
[ {mechanism, emqx_authn_schema:mechanism('password-based')}
, {backend, emqx_authn_schema:backend(postgresql)}
, {password_hash_algorithm, fun password_hash_algorithm/1}
, {salt_position, fun salt_position/1}
, {password_hash_algorithm, fun emqx_authn_password_hashing:type_ro/1}
, {query, fun query/1}
] ++ emqx_authn_schema:common_fields()
++ emqx_connector_schema_lib:relational_db_fields()
++ emqx_connector_schema_lib:ssl_fields().
password_hash_algorithm(type) -> {enum, [plain, md5, sha, sha256, sha512, bcrypt]};
password_hash_algorithm(default) -> sha256;
password_hash_algorithm(_) -> undefined.
salt_position(type) -> {enum, [prefix, suffix]};
salt_position(default) -> prefix;
salt_position(_) -> undefined.
query(type) -> string();
query(_) -> undefined.
@ -81,14 +72,13 @@ create(_AuthenticatorID, Config) ->
create(Config).
create(#{query := Query0,
password_hash_algorithm := Algorithm,
salt_position := SaltPosition} = Config) ->
password_hash_algorithm := Algorithm} = Config) ->
ok = emqx_authn_password_hashing:init(Algorithm),
{Query, PlaceHolders} = parse_query(Query0),
ResourceId = emqx_authn_utils:make_resource_id(?MODULE),
State = #{query => Query,
placeholders => PlaceHolders,
password_hash_algorithm => Algorithm,
salt_position => SaltPosition,
resource_id => ResourceId},
case emqx_resource:create_local(ResourceId, emqx_connector_pgsql, Config) of
{ok, already_created} ->
@ -113,14 +103,16 @@ authenticate(#{auth_method := _}, _) ->
authenticate(#{password := Password} = Credential,
#{query := Query,
placeholders := PlaceHolders,
resource_id := ResourceId} = State) ->
resource_id := ResourceId,
password_hash_algorithm := Algorithm}) ->
Params = emqx_authn_utils:replace_placeholders(PlaceHolders, Credential),
case emqx_resource:query(ResourceId, {sql, Query, Params}) of
{ok, _Columns, []} -> ignore;
{ok, Columns, [Row | _]} ->
NColumns = [Name || #column{name = Name} <- Columns],
Selected = maps:from_list(lists:zip(NColumns, erlang:tuple_to_list(Row))),
case emqx_authn_utils:check_password(Password, Selected, State) of
case emqx_authn_utils:check_password_from_selected_map(
Algorithm, Selected, Password) of
ok ->
{ok, emqx_authn_utils:is_superuser(Selected)};
{error, Reason} ->

View File

@ -59,21 +59,12 @@ common_fields() ->
[ {mechanism, emqx_authn_schema:mechanism('password-based')}
, {backend, emqx_authn_schema:backend(redis)}
, {cmd, fun cmd/1}
, {password_hash_algorithm, fun password_hash_algorithm/1}
, {salt_position, fun salt_position/1}
, {password_hash_algorithm, fun emqx_authn_password_hashing:type_ro/1}
] ++ emqx_authn_schema:common_fields().
cmd(type) -> string();
cmd(_) -> undefined.
password_hash_algorithm(type) -> {enum, [plain, md5, sha, sha256, sha512, bcrypt]};
password_hash_algorithm(default) -> sha256;
password_hash_algorithm(_) -> undefined.
salt_position(type) -> {enum, [prefix, suffix]};
salt_position(default) -> prefix;
salt_position(_) -> undefined.
%%------------------------------------------------------------------------------
%% APIs
%%------------------------------------------------------------------------------
@ -89,6 +80,7 @@ create(_AuthenticatorID, Config) ->
create(#{cmd := Cmd,
password_hash_algorithm := Algorithm} = Config) ->
ok = emqx_authn_password_hashing:init(Algorithm),
try
NCmd = parse_cmd(Cmd),
ok = emqx_authn_utils:ensure_apps_started(Algorithm),
@ -129,13 +121,15 @@ authenticate(#{auth_method := _}, _) ->
ignore;
authenticate(#{password := Password} = Credential,
#{cmd := {Command, Key, Fields},
resource_id := ResourceId} = State) ->
resource_id := ResourceId,
password_hash_algorithm := Algorithm}) ->
NKey = binary_to_list(iolist_to_binary(replace_placeholders(Key, Credential))),
case emqx_resource:query(ResourceId, {cmd, [Command, NKey | Fields]}) of
{ok, Values} ->
case merge(Fields, Values) of
#{<<"password_hash">> := _} = Selected ->
case emqx_authn_utils:check_password(Password, Selected, State) of
case emqx_authn_utils:check_password_from_selected_map(
Algorithm, Selected, Password) of
ok ->
{ok, emqx_authn_utils:is_superuser(Selected)};
{error, Reason} ->

View File

@ -238,22 +238,22 @@ test_is_superuser({Value, ExpectedValue}) ->
raw_mongo_auth_config() ->
#{
mechanism => <<"password-based">>,
password_hash_algorithm => <<"plain">>,
salt_position => <<"suffix">>,
enable => <<"true">>,
mechanism => <<"password-based">>,
password_hash_algorithm => #{name => <<"plain">>,
salt_position => <<"suffix">>},
enable => <<"true">>,
backend => <<"mongodb">>,
mongo_type => <<"single">>,
database => <<"mqtt">>,
collection => <<"users">>,
server => mongo_server(),
backend => <<"mongodb">>,
mongo_type => <<"single">>,
database => <<"mqtt">>,
collection => <<"users">>,
server => mongo_server(),
selector => #{<<"username">> => <<"${username}">>},
password_hash_field => <<"password_hash">>,
salt_field => <<"salt">>,
is_superuser_field => <<"is_superuser">>
}.
selector => #{<<"username">> => <<"${username}">>},
password_hash_field => <<"password_hash">>,
salt_field => <<"salt">>,
is_superuser_field => <<"is_superuser">>
}.
user_seeds() ->
[#{data => #{
@ -282,8 +282,8 @@ user_seeds() ->
password => <<"md5">>
},
config_params => #{
password_hash_algorithm => <<"md5">>,
salt_position => <<"suffix">>
password_hash_algorithm => #{name => <<"md5">>,
salt_position => <<"suffix">> }
},
result => {ok,#{is_superuser => false}}
},
@ -300,8 +300,8 @@ user_seeds() ->
},
config_params => #{
selector => #{<<"username">> => <<"${clientid}">>},
password_hash_algorithm => <<"sha256">>,
salt_position => <<"prefix">>
password_hash_algorithm => #{name => <<"sha256">>,
salt_position => <<"prefix">>}
},
result => {ok,#{is_superuser => true}}
},
@ -317,8 +317,7 @@ user_seeds() ->
password => <<"bcrypt">>
},
config_params => #{
password_hash_algorithm => <<"bcrypt">>,
salt_position => <<"suffix">> % should be ignored
password_hash_algorithm => #{name => <<"bcrypt">>}
},
result => {ok,#{is_superuser => false}}
},
@ -336,8 +335,7 @@ user_seeds() ->
config_params => #{
% clientid variable & username credentials
selector => #{<<"username">> => <<"${clientid}">>},
password_hash_algorithm => <<"bcrypt">>,
salt_position => <<"suffix">>
password_hash_algorithm => #{name => <<"bcrypt">>}
},
result => {error,not_authorized}
},
@ -354,8 +352,7 @@ user_seeds() ->
},
config_params => #{
selector => #{<<"userid">> => <<"${clientid}">>},
password_hash_algorithm => <<"bcrypt">>,
salt_position => <<"suffix">>
password_hash_algorithm => #{name => <<"bcrypt">>}
},
result => {error,not_authorized}
},
@ -372,8 +369,7 @@ user_seeds() ->
password => <<"wrongpass">>
},
config_params => #{
password_hash_algorithm => <<"bcrypt">>,
salt_position => <<"suffix">>
password_hash_algorithm => #{name => <<"bcrypt">>}
},
result => {error,bad_username_or_password}
}

View File

@ -204,20 +204,20 @@ t_update(_Config) ->
raw_mysql_auth_config() ->
#{
mechanism => <<"password-based">>,
password_hash_algorithm => <<"plain">>,
salt_position => <<"suffix">>,
enable => <<"true">>,
mechanism => <<"password-based">>,
password_hash_algorithm => #{name => <<"plain">>,
salt_position => <<"suffix">>},
enable => <<"true">>,
backend => <<"mysql">>,
database => <<"mqtt">>,
username => <<"root">>,
password => <<"public">>,
backend => <<"mysql">>,
database => <<"mqtt">>,
username => <<"root">>,
password => <<"public">>,
query => <<"SELECT password_hash, salt, is_superuser_str as is_superuser
query => <<"SELECT password_hash, salt, is_superuser_str as is_superuser
FROM users where username = ${username} LIMIT 1">>,
server => mysql_server()
}.
server => mysql_server()
}.
user_seeds() ->
[#{data => #{
@ -244,8 +244,8 @@ user_seeds() ->
password => <<"md5">>
},
config_params => #{
password_hash_algorithm => <<"md5">>,
salt_position => <<"suffix">>
password_hash_algorithm => #{name => <<"md5">>,
salt_position => <<"suffix">>}
},
result => {ok,#{is_superuser => false}}
},
@ -263,8 +263,8 @@ user_seeds() ->
config_params => #{
query => <<"SELECT password_hash, salt, is_superuser_int as is_superuser
FROM users where username = ${clientid} LIMIT 1">>,
password_hash_algorithm => <<"sha256">>,
salt_position => <<"prefix">>
password_hash_algorithm => #{name => <<"sha256">>,
salt_position => <<"prefix">>}
},
result => {ok,#{is_superuser => true}}
},
@ -282,8 +282,7 @@ user_seeds() ->
config_params => #{
query => <<"SELECT password_hash, salt, is_superuser_int as is_superuser
FROM users where username = ${username} LIMIT 1">>,
password_hash_algorithm => <<"bcrypt">>,
salt_position => <<"suffix">> % should be ignored
password_hash_algorithm => #{name => <<"bcrypt">>}
},
result => {ok,#{is_superuser => false}}
},
@ -300,8 +299,7 @@ user_seeds() ->
config_params => #{
query => <<"SELECT password_hash, salt, is_superuser_int as is_superuser
FROM users where username = ${username} LIMIT 1">>,
password_hash_algorithm => <<"bcrypt">>,
salt_position => <<"suffix">> % should be ignored
password_hash_algorithm => #{name => <<"bcrypt">>}
},
result => {ok,#{is_superuser => false}}
},
@ -320,8 +318,7 @@ user_seeds() ->
% clientid variable & username credentials
query => <<"SELECT password_hash, salt, is_superuser_int as is_superuser
FROM users where username = ${clientid} LIMIT 1">>,
password_hash_algorithm => <<"bcrypt">>,
salt_position => <<"suffix">>
password_hash_algorithm => #{name => <<"bcrypt">>}
},
result => {error,not_authorized}
},
@ -340,8 +337,7 @@ user_seeds() ->
% Bad keys in query
query => <<"SELECT 1 AS unknown_field
FROM users where username = ${username} LIMIT 1">>,
password_hash_algorithm => <<"bcrypt">>,
salt_position => <<"suffix">>
password_hash_algorithm => #{name => <<"bcrypt">>}
},
result => {error,not_authorized}
},
@ -358,8 +354,7 @@ user_seeds() ->
password => <<"wrongpass">>
},
config_params => #{
password_hash_algorithm => <<"bcrypt">>,
salt_position => <<"suffix">>
password_hash_algorithm => #{name => <<"bcrypt">>}
},
result => {error,bad_username_or_password}
}

View File

@ -0,0 +1,155 @@
%%--------------------------------------------------------------------
%% 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_authn_password_hashing_SUITE).
-compile(nowarn_export_all).
-compile(export_all).
-include_lib("eunit/include/eunit.hrl").
-include_lib("common_test/include/ct.hrl").
-define(SIMPLE_HASHES, [plain, md5, sha, sha256, sha512]).
all() ->
emqx_common_test_helpers:all(?MODULE).
groups() ->
[].
init_per_suite(Config) ->
{ok, _} = application:ensure_all_started(bcrypt),
Config.
end_per_suite(_Config) ->
ok.
t_gen_salt(_Config) ->
Algorithms = [#{name => Type, salt_position => suffix} || Type <- ?SIMPLE_HASHES]
++ [#{name => bcrypt, salt_rounds => 10}],
lists:foreach(
fun(Algorithm) ->
Salt = emqx_authn_password_hashing:gen_salt(Algorithm),
ct:pal("gen_salt(~p): ~p", [Algorithm, Salt]),
?assert(is_binary(Salt))
end,
Algorithms).
t_init(_Config) ->
Algorithms = [#{name => Type, salt_position => suffix} || Type <- ?SIMPLE_HASHES]
++ [#{name => bcrypt, salt_rounds => 10}],
lists:foreach(
fun(Algorithm) ->
ok = emqx_authn_password_hashing:init(Algorithm)
end,
Algorithms).
t_check_password(_Config) ->
lists:foreach(
fun test_check_password/1,
hash_examples()).
test_check_password(#{
password_hash := Hash,
salt := Salt,
password := Password,
password_hash_algorithm := Algorithm
} = Sample) ->
ct:pal("t_check_password sample: ~p", [Sample]),
true = emqx_authn_password_hashing:check_password(Algorithm, Salt, Hash, Password),
false = emqx_authn_password_hashing:check_password(Algorithm, Salt, Hash, <<"wrongpass">>).
t_hash(_Config) ->
lists:foreach(
fun test_hash/1,
hash_examples()).
test_hash(#{password := Password,
password_hash_algorithm := Algorithm
} = Sample) ->
ct:pal("t_hash sample: ~p", [Sample]),
{Hash, Salt} = emqx_authn_password_hashing:hash(Algorithm, Password),
true = emqx_authn_password_hashing:check_password(Algorithm, Salt, Hash, Password).
hash_examples() ->
[#{
password_hash => <<"plainsalt">>,
salt => <<"salt">>,
password => <<"plain">>,
password_hash_algorithm => #{name => plain,
salt_position => suffix}
},
#{
password_hash => <<"9b4d0c43d206d48279e69b9ad7132e22">>,
salt => <<"salt">>,
password => <<"md5">>,
password_hash_algorithm => #{name => md5,
salt_position => suffix}
},
#{
password_hash => <<"c665d4c0a9e5498806b7d9fd0b417d272853660e">>,
salt => <<"salt">>,
password => <<"sha">>,
password_hash_algorithm => #{name => sha,
salt_position => prefix}
},
#{
password_hash => <<"ac63a624e7074776d677dd61a003b8c803eb11db004d0ec6ae032a5d7c9c5caf">>,
salt => <<"salt">>,
password => <<"sha256">>,
password_hash_algorithm => #{name => sha256,
salt_position => prefix}
},
#{
password_hash => <<"a1509ab67bfacbad020927b5ac9d91e9100a82e33a0ebb01459367ce921c0aa8"
"157aa5652f94bc84fa3babc08283e44887d61c48bcf8ad7bcb3259ee7d0eafcd">>,
salt => <<"salt">>,
password => <<"sha512">>,
password_hash_algorithm => #{name => sha512,
salt_position => prefix}
},
#{
password_hash => <<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>,
salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>,
password => <<"bcrypt">>,
password_hash_algorithm => #{name => bcrypt,
salt_rounds => 10}
},
#{
password_hash => <<"01dbee7f4a9e243e988b62c73cda935d"
"a05378b93244ec8f48a99e61ad799d86">>,
salt => <<"ATHENA.MIT.EDUraeburn">>,
password => <<"password">>,
password_hash_algorithm => #{name => pbkdf2,
iterations => 2,
dk_length => 32,
mac_fun => sha}
},
#{
password_hash => <<"01dbee7f4a9e243e988b62c73cda935da05378b9">>,
salt => <<"ATHENA.MIT.EDUraeburn">>,
password => <<"password">>,
password_hash_algorithm => #{name => pbkdf2,
iterations => 2,
mac_fun => sha}
}
].

View File

@ -272,20 +272,20 @@ t_parse_query(_) ->
raw_pgsql_auth_config() ->
#{
mechanism => <<"password-based">>,
password_hash_algorithm => <<"plain">>,
salt_position => <<"suffix">>,
enable => <<"true">>,
mechanism => <<"password-based">>,
password_hash_algorithm => #{name => <<"plain">>,
salt_position => <<"suffix">>},
enable => <<"true">>,
backend => <<"postgresql">>,
database => <<"mqtt">>,
username => <<"root">>,
password => <<"public">>,
backend => <<"postgresql">>,
database => <<"mqtt">>,
username => <<"root">>,
password => <<"public">>,
query => <<"SELECT password_hash, salt, is_superuser_str as is_superuser
query => <<"SELECT password_hash, salt, is_superuser_str as is_superuser
FROM users where username = ${username} LIMIT 1">>,
server => pgsql_server()
}.
server => pgsql_server()
}.
user_seeds() ->
[#{data => #{
@ -312,8 +312,8 @@ user_seeds() ->
password => <<"md5">>
},
config_params => #{
password_hash_algorithm => <<"md5">>,
salt_position => <<"suffix">>
password_hash_algorithm => #{name => <<"md5">>,
salt_position => <<"suffix">>}
},
result => {ok,#{is_superuser => false}}
},
@ -331,8 +331,8 @@ user_seeds() ->
config_params => #{
query => <<"SELECT password_hash, salt, is_superuser_int as is_superuser
FROM users where username = ${clientid} LIMIT 1">>,
password_hash_algorithm => <<"sha256">>,
salt_position => <<"prefix">>
password_hash_algorithm => #{name => <<"sha256">>,
salt_position => <<"prefix">>}
},
result => {ok,#{is_superuser => true}}
},
@ -350,8 +350,7 @@ user_seeds() ->
config_params => #{
query => <<"SELECT password_hash, salt, is_superuser_int as is_superuser
FROM users where username = ${username} LIMIT 1">>,
password_hash_algorithm => <<"bcrypt">>,
salt_position => <<"suffix">> % should be ignored
password_hash_algorithm => #{name => <<"bcrypt">>}
},
result => {ok,#{is_superuser => false}}
},
@ -370,8 +369,7 @@ user_seeds() ->
% clientid variable & username credentials
query => <<"SELECT password_hash, salt, is_superuser_int as is_superuser
FROM users where username = ${clientid} LIMIT 1">>,
password_hash_algorithm => <<"bcrypt">>,
salt_position => <<"suffix">>
password_hash_algorithm => #{name => <<"bcrypt">>}
},
result => {error,not_authorized}
},
@ -390,8 +388,7 @@ user_seeds() ->
% Bad keys in query
query => <<"SELECT 1 AS unknown_field
FROM users where username = ${username} LIMIT 1">>,
password_hash_algorithm => <<"bcrypt">>,
salt_position => <<"suffix">>
password_hash_algorithm => #{name => <<"bcrypt">>}
},
result => {error,not_authorized}
},
@ -408,8 +405,7 @@ user_seeds() ->
password => <<"wrongpass">>
},
config_params => #{
password_hash_algorithm => <<"bcrypt">>,
salt_position => <<"suffix">>
password_hash_algorithm => #{name => <<"bcrypt">>}
},
result => {error,bad_username_or_password}
}

View File

@ -208,137 +208,150 @@ t_update(_Config) ->
raw_redis_auth_config() ->
#{
mechanism => <<"password-based">>,
password_hash_algorithm => <<"plain">>,
salt_position => <<"suffix">>,
enable => <<"true">>,
mechanism => <<"password-based">>,
password_hash_algorithm => #{name => <<"plain">>,
salt_position => <<"suffix">>},
enable => <<"true">>,
backend => <<"redis">>,
cmd => <<"HMGET mqtt_user:${username} password_hash salt is_superuser">>,
database => <<"1">>,
password => <<"public">>,
server => redis_server()
}.
backend => <<"redis">>,
cmd => <<"HMGET mqtt_user:${username} password_hash salt is_superuser">>,
database => <<"1">>,
password => <<"public">>,
server => redis_server()
}.
user_seeds() ->
[#{data => #{
password_hash => "plainsalt",
salt => "salt",
is_superuser => "1"
password_hash => <<"plainsalt">>,
salt => <<"salt">>,
is_superuser => <<"1">>
},
credentials => #{
username => <<"plain">>,
password => <<"plain">>},
key => "mqtt_user:plain",
key => <<"mqtt_user:plain">>,
config_params => #{},
result => {ok,#{is_superuser => true}}
},
#{data => #{
password_hash => "9b4d0c43d206d48279e69b9ad7132e22",
salt => "salt",
is_superuser => "0"
password_hash => <<"9b4d0c43d206d48279e69b9ad7132e22">>,
salt => <<"salt">>,
is_superuser => <<"0">>
},
credentials => #{
username => <<"md5">>,
password => <<"md5">>
},
key => "mqtt_user:md5",
key => <<"mqtt_user:md5">>,
config_params => #{
password_hash_algorithm => <<"md5">>,
salt_position => <<"suffix">>
password_hash_algorithm => #{name => <<"md5">>,
salt_position => <<"suffix">>}
},
result => {ok,#{is_superuser => false}}
},
#{data => #{
password_hash => "ac63a624e7074776d677dd61a003b8c803eb11db004d0ec6ae032a5d7c9c5caf",
salt => "salt",
is_superuser => "1"
password_hash => <<"ac63a624e7074776d677dd61a003b8c803eb11db004d0ec6ae032a5d7c9c5caf">>,
salt => <<"salt">>,
is_superuser => <<"1">>
},
credentials => #{
clientid => <<"sha256">>,
password => <<"sha256">>
},
key => "mqtt_user:sha256",
key => <<"mqtt_user:sha256">>,
config_params => #{
cmd => <<"HMGET mqtt_user:${clientid} password_hash salt is_superuser">>,
password_hash_algorithm => <<"sha256">>,
salt_position => <<"prefix">>
password_hash_algorithm => #{name => <<"sha256">>,
salt_position => <<"prefix">>}
},
result => {ok,#{is_superuser => true}}
},
#{data => #{
password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u",
salt => "$2b$12$wtY3h20mUjjmeaClpqZVve",
is_superuser => "0"
password_hash => <<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>,
salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>,
is_superuser => <<"0">>
},
credentials => #{
username => <<"bcrypt">>,
password => <<"bcrypt">>
},
key => "mqtt_user:bcrypt",
key => <<"mqtt_user:bcrypt">>,
config_params => #{
password_hash_algorithm => <<"bcrypt">>,
salt_position => <<"suffix">> % should be ignored
password_hash_algorithm => #{name => <<"bcrypt">>}
},
result => {ok,#{is_superuser => false}}
},
#{data => #{
password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u",
salt => "$2b$12$wtY3h20mUjjmeaClpqZVve",
is_superuser => "0"
password_hash => <<"01dbee7f4a9e243e988b62c73cda935da05378b9">>,
salt => <<"ATHENA.MIT.EDUraeburn">>,
is_superuser => <<"0">>
},
credentials => #{
username => <<"pbkdf2">>,
password => <<"password">>
},
key => <<"mqtt_user:pbkdf2">>,
config_params => #{
password_hash_algorithm => #{name => <<"pbkdf2">>,
iterations => 2,
mac_fun => sha
}
},
result => {ok,#{is_superuser => false}}
},
#{data => #{
password_hash => <<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>,
salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>,
is_superuser => <<"0">>
},
credentials => #{
username => <<"bcrypt0">>,
password => <<"bcrypt">>
},
key => "mqtt_user:bcrypt0",
key => <<"mqtt_user:bcrypt0">>,
config_params => #{
% clientid variable & username credentials
cmd => <<"HMGET mqtt_client:${clientid} password_hash salt is_superuser">>,
password_hash_algorithm => <<"bcrypt">>,
salt_position => <<"suffix">>
password_hash_algorithm => #{name => <<"bcrypt">>}
},
result => {error,not_authorized}
},
#{data => #{
password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u",
salt => "$2b$12$wtY3h20mUjjmeaClpqZVve",
is_superuser => "0"
password_hash => <<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>,
salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>,
is_superuser => <<"0">>
},
credentials => #{
username => <<"bcrypt1">>,
password => <<"bcrypt">>
},
key => "mqtt_user:bcrypt1",
key => <<"mqtt_user:bcrypt1">>,
config_params => #{
% Bad key in cmd
cmd => <<"HMGET badkey:${username} password_hash salt is_superuser">>,
password_hash_algorithm => <<"bcrypt">>,
salt_position => <<"suffix">>
password_hash_algorithm => #{name => <<"bcrypt">>}
},
result => {error,not_authorized}
},
#{data => #{
password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u",
salt => "$2b$12$wtY3h20mUjjmeaClpqZVve",
is_superuser => "0"
password_hash => <<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>,
salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>,
is_superuser => <<"0">>
},
credentials => #{
username => <<"bcrypt2">>,
% Wrong password
password => <<"wrongpass">>
},
key => "mqtt_user:bcrypt2",
key => <<"mqtt_user:bcrypt2">>,
config_params => #{
cmd => <<"HMGET mqtt_user:${username} password_hash salt is_superuser">>,
password_hash_algorithm => <<"bcrypt">>,
salt_position => <<"suffix">>
password_hash_algorithm => #{name => <<"bcrypt">>}
},
result => {error,bad_username_or_password}
}