diff --git a/apps/emqx/src/emqx_channel.erl b/apps/emqx/src/emqx_channel.erl index 4e48be2cb..eb71aca58 100644 --- a/apps/emqx/src/emqx_channel.erl +++ b/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, diff --git a/apps/emqx/src/emqx_passwd.erl b/apps/emqx/src/emqx_passwd.erl index 2104f1850..2f9775d6d 100644 --- a/apps/emqx/src/emqx_passwd.erl +++ b/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()). --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, <>)); -check_pass({PassHash, Salt, Password}, {HashType, salt}) -> - check_pass(PassHash, emqx_passwd:hash(HashType, <>)); -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, <>); +hash({SimpleHash, Salt, suffix}, Password) when is_binary(Password), is_binary(Salt) -> + hash_data(SimpleHash, <>). + + +-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(<>) -> - iolist_to_binary(io_lib:format("~32.16.0b", [X])); -hexstring(<>) -> - iolist_to_binary(io_lib:format("~40.16.0b", [X])); -hexstring(<>) -> - iolist_to_binary(io_lib:format("~64.16.0b", [X])); -hexstring(<>) -> - 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). diff --git a/apps/emqx/test/emqx_passwd_SUITE.erl b/apps/emqx/test/emqx_passwd_SUITE.erl index fe4694294..066912ba1 100644 --- a/apps/emqx/test/emqx_passwd_SUITE.erl +++ b/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)). diff --git a/apps/emqx_authn/src/emqx_authn_password_hashing.erl b/apps/emqx_authn/src/emqx_authn_password_hashing.erl new file mode 100644 index 000000000..9e3637285 --- /dev/null +++ b/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 -> + <> = 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)]. diff --git a/apps/emqx_authn/src/emqx_authn_utils.erl b/apps/emqx_authn/src/emqx_authn_utils.erl index 2205d237d..b211cc1bd 100644 --- a/apps/emqx_authn/src/emqx_authn_utils.erl +++ b/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, <>); -hash(Algorithm, Password, Salt, suffix) -> - emqx_passwd:hash(Algorithm, <>). - -gen_salt() -> - <> = 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. diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl index f609d8cac..2c68c034d 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl +++ b/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), + 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, <>). - -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, diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl index 3b47bcd7b..8f8b53f14 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl +++ b/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, <>); -hash(Algorithm, Password, Salt, suffix) -> - emqx_passwd:hash(Algorithm, <>). - -to_list(L) when is_list(L) -> L; -to_list(L) when is_binary(L) -> binary_to_list(L); -to_list(X) -> X. diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl index fd0d09f57..852789363 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl +++ b/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} -> diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl index fdd30b618..0ed7d282a 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl +++ b/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} -> diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl index e17d0ad8f..1927ab822 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl +++ b/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} -> diff --git a/apps/emqx_authn/test/emqx_authn_mongo_SUITE.erl b/apps/emqx_authn/test/emqx_authn_mongo_SUITE.erl index 562c5aa1b..edd91be55 100644 --- a/apps/emqx_authn/test/emqx_authn_mongo_SUITE.erl +++ b/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">>, + 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} } diff --git a/apps/emqx_authn/test/emqx_authn_mysql_SUITE.erl b/apps/emqx_authn/test/emqx_authn_mysql_SUITE.erl index bf66b034a..95eecdead 100644 --- a/apps/emqx_authn/test/emqx_authn_mysql_SUITE.erl +++ b/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} } diff --git a/apps/emqx_authn/test/emqx_authn_password_hashing_SUITE.erl b/apps/emqx_authn/test/emqx_authn_password_hashing_SUITE.erl new file mode 100644 index 000000000..8832c551d --- /dev/null +++ b/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} + } + ]. diff --git a/apps/emqx_authn/test/emqx_authn_pgsql_SUITE.erl b/apps/emqx_authn/test/emqx_authn_pgsql_SUITE.erl index 2a79179e1..8f1f12690 100644 --- a/apps/emqx_authn/test/emqx_authn_pgsql_SUITE.erl +++ b/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} } diff --git a/apps/emqx_authn/test/emqx_authn_redis_SUITE.erl b/apps/emqx_authn/test/emqx_authn_redis_SUITE.erl index 2e941e72f..de556a7bd 100644 --- a/apps/emqx_authn/test/emqx_authn_redis_SUITE.erl +++ b/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">>, + 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} }