Просмотр исходного кода

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

refactor(authn): unify password hashing
Ilya Averyanov 4 лет назад
Родитель
Сommit
b8a68d7a9f

+ 1 - 1
apps/emqx/src/emqx_channel.erl

@@ -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,

+ 97 - 58
apps/emqx/src/emqx_passwd.erl

@@ -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()).
+
+-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()).
 
--export_type([hash_type/0]).
+-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}.
-
--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}),
-            <<>>
+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_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}),
-            <<>>
-    end.
+            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 funcs
+%% Internal functions
 %%--------------------------------------------------------------------
 
-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])).
+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.
+
+compare_secure([X | RestX], [Y | RestY], Result) ->
+	compare_secure(RestX, RestY, (X bxor Y) bor Result);
+compare_secure([], [], Result) ->
+	Result == 0.
+
+
+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).

+ 80 - 8
apps/emqx/test/emqx_passwd_SUITE.erl

@@ -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)).

+ 167 - 0
apps/emqx_authn/src/emqx_authn_password_hashing.erl

@@ -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)].

+ 13 - 34
apps/emqx_authn/src/emqx_authn_utils.erl

@@ -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.

+ 19 - 68
apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl

@@ -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),
-    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),
+         password_hash_algorithm := Algorithm}) ->
+    ok = emqx_authn_password_hashing:init(Algorithm),
     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,

+ 4 - 36
apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl

@@ -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.

+ 6 - 14
apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl

@@ -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} ->

+ 7 - 15
apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl

@@ -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} ->

+ 6 - 12
apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl

@@ -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} ->

+ 24 - 28
apps/emqx_authn/test/emqx_authn_mongo_SUITE.erl

@@ -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">>,
-
-        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">>
-    }.
+      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(),
+
+      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}
       }

+ 20 - 25
apps/emqx_authn/test/emqx_authn_mysql_SUITE.erl

@@ -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}
       }

+ 155 - 0
apps/emqx_authn/test/emqx_authn_password_hashing_SUITE.erl

@@ -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}
+      }
+    ].

+ 19 - 23
apps/emqx_authn/test/emqx_authn_pgsql_SUITE.erl

@@ -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}
       }

+ 65 - 52
apps/emqx_authn/test/emqx_authn_redis_SUITE.erl

@@ -208,137 +208,150 @@ t_update(_Config) ->
 
 raw_redis_auth_config() ->
     #{
-        mechanism => <<"password-based">>,
-        password_hash_algorithm => <<"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()
-    }.
+      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()
+     }.
 
 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 => <<"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"
+                 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}
       }