refactor(authn): restore pbkdf2 password hashing functionality

This commit is contained in:
Ilya Averyanov 2021-12-13 14:53:34 +03:00
parent 2b0a3e8ba3
commit 708d9cfc6c
5 changed files with 152 additions and 52 deletions

View File

@ -34,18 +34,34 @@
-type(password_hash() :: binary()). -type(password_hash() :: binary()).
-type(hash_type_simple() :: plain | md5 | sha | sha256 | sha512). -type(hash_type_simple() :: plain | md5 | sha | sha256 | sha512).
-type(hash_type() :: hash_type_simple() | bcrypt). -type(hash_type() :: hash_type_simple() | bcrypt | pbkdf2).
-type(salt_position() :: prefix | suffix). -type(salt_position() :: prefix | suffix).
-type(salt() :: binary()). -type(salt() :: binary()).
-type(hash_params() :: {bcrypt, salt()} | {hash_type_simple(), salt(), salt_position()}). -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 %% APIs
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
-spec(check_pass(hash_params(), password_hash(), password()) -> boolean()). -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({bcrypt, Salt}, PasswordHash, Password) -> check_pass({bcrypt, Salt}, PasswordHash, Password) ->
case bcrypt:hashpw(Password, Salt) of case bcrypt:hashpw(Password, Salt) of
{ok, HashPasswd} -> {ok, HashPasswd} ->
@ -58,6 +74,13 @@ check_pass({_SimpleHash, _Salt, _SaltPosition} = HashParams, PasswordHash, Passw
compare_secure(Hash, PasswordHash). compare_secure(Hash, PasswordHash).
-spec(hash(hash_params(), password()) -> password_hash()). -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) -> hash({bcrypt, Salt}, Password) ->
case bcrypt:hashpw(Password, Salt) of case bcrypt:hashpw(Password, Salt) of
{ok, HashPasswd} -> {ok, HashPasswd} ->
@ -75,13 +98,13 @@ hash({SimpleHash, Salt, suffix}, Password) when is_binary(Password), is_binary(S
hash_data(plain, Data) when is_binary(Data) -> hash_data(plain, Data) when is_binary(Data) ->
Data; Data;
hash_data(md5, Data) when is_binary(Data) -> hash_data(md5, Data) when is_binary(Data) ->
hexstring(crypto:hash(md5, Data)); hex(crypto:hash(md5, Data));
hash_data(sha, Data) when is_binary(Data) -> hash_data(sha, Data) when is_binary(Data) ->
hexstring(crypto:hash(sha, Data)); hex(crypto:hash(sha, Data));
hash_data(sha256, Data) when is_binary(Data) -> hash_data(sha256, Data) when is_binary(Data) ->
hexstring(crypto:hash(sha256, Data)); hex(crypto:hash(sha256, Data));
hash_data(sha512, Data) when is_binary(Data) -> hash_data(sha512, Data) when is_binary(Data) ->
hexstring(crypto:hash(sha512, Data)). hex(crypto:hash(sha512, Data)).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Internal functions %% Internal functions
@ -103,11 +126,11 @@ compare_secure([], [], Result) ->
Result == 0. Result == 0.
hexstring(<<X:128/big-unsigned-integer>>) -> pbkdf2(MacFun, Password, Salt, Iterations, undefined) ->
iolist_to_binary(io_lib:format("~32.16.0b", [X])); pbkdf2:pbkdf2(MacFun, Password, Salt, Iterations);
hexstring(<<X:160/big-unsigned-integer>>) -> pbkdf2(MacFun, Password, Salt, Iterations, DKLength) ->
iolist_to_binary(io_lib:format("~40.16.0b", [X])); pbkdf2:pbkdf2(MacFun, Password, Salt, Iterations, DKLength).
hexstring(<<X:256/big-unsigned-integer>>) ->
iolist_to_binary(io_lib:format("~64.16.0b", [X]));
hexstring(<<X:512/big-unsigned-integer>>) -> hex(X) when is_binary(X) ->
iolist_to_binary(io_lib:format("~128.16.0b", [X])). pbkdf2:to_hex(X).

View File

@ -88,4 +88,16 @@ t_hash(_) ->
false = emqx_passwd:check_pass({bcrypt, <<>>}, <<>>, WrongPassword), false = emqx_passwd:check_pass({bcrypt, <<>>}, <<>>, WrongPassword),
%% Invalid salt, bcrypt fails %% Invalid salt, bcrypt fails
?assertException(error, _, emqx_passwd:hash({bcrypt, Salt}, Password)). ?assertException(error, _, emqx_passwd:hash({bcrypt, Salt}, Password)),
BadDKlen = 1 bsl 32,
Pbkdf2Salt = <<"ATHENA.MIT.EDUraeburn">>,
Pbkdf2 = <<"01dbee7f4a9e243e988b62c73cda935d"
"a05378b93244ec8f48a99e61ad799d86">>,
Pbkdf2 = emqx_passwd:hash({pbkdf2, sha, Pbkdf2Salt, 2, 32}, Password),
true = emqx_passwd:check_pass({pbkdf2, sha, Pbkdf2Salt, 2, 32}, Pbkdf2, Password),
false = emqx_passwd:check_pass({pbkdf2, sha, Pbkdf2Salt, 2, 32}, Pbkdf2, WrongPassword),
false = emqx_passwd:check_pass({pbkdf2, sha, Pbkdf2Salt, 2, BadDKlen}, Pbkdf2, Password),
%% Invalid derived_length, pbkdf2 fails
?assertException(error, _, emqx_passwd:hash({pbkdf2, sha, Pbkdf2Salt, 2, BadDKlen}, Password)).

View File

@ -27,8 +27,12 @@
-type(bcrypt_algorithm() :: #{name := bcrypt}). -type(bcrypt_algorithm() :: #{name := bcrypt}).
-type(bcrypt_algorithm_rw() :: #{name := bcrypt, salt_rounds := integer()}). -type(bcrypt_algorithm_rw() :: #{name := bcrypt, salt_rounds := integer()}).
-type(algorithm() :: simple_algorithm() | bcrypt_algorithm()). -type(pbkdf2_algorithm() :: #{name := pbkdf2,
-type(algorithm_rw() :: simple_algorithm() | bcrypt_algorithm_rw()). 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 %% Hocon Schema
@ -47,7 +51,7 @@
hash/2, hash/2,
check_password/4]). check_password/4]).
roots() -> [bcrypt, bcrypt_rw, other_algorithms]. roots() -> [pbkdf2, bcrypt, bcrypt_rw, other_algorithms].
fields(bcrypt_rw) -> fields(bcrypt_rw) ->
fields(bcrypt) ++ fields(bcrypt) ++
@ -56,6 +60,12 @@ fields(bcrypt_rw) ->
fields(bcrypt) -> fields(bcrypt) ->
[{name, {enum, [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) -> fields(other_algorithms) ->
[{name, {enum, [plain, md5, sha, sha256, sha512]}}, [{name, {enum, [plain, md5, sha, sha256, sha512]}},
{salt_position, fun salt_position/1}]. {salt_position, fun salt_position/1}].
@ -68,6 +78,11 @@ salt_rounds(type) -> integer();
salt_rounds(default) -> 10; salt_rounds(default) -> 10;
salt_rounds(_) -> undefined. salt_rounds(_) -> undefined.
dk_length(type) -> integer();
dk_length(nullable) -> true;
dk_length(default) -> undefined;
dk_length(_) -> undefined.
type_rw(type) -> type_rw(type) ->
hoconsc:union(rw_refs()); hoconsc:union(rw_refs());
type_rw(default) -> #{<<"name">> => sha256, <<"salt_position">> => prefix}; type_rw(default) -> #{<<"name">> => sha256, <<"salt_position">> => prefix};
@ -108,7 +123,13 @@ hash(#{name := bcrypt, salt_rounds := _} = Algorithm, Password) ->
Hash = emqx_passwd:hash({bcrypt, Salt0}, Password), Hash = emqx_passwd:hash({bcrypt, Salt0}, Password),
Salt = Hash, Salt = Hash,
{Hash, Salt}; {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) -> hash(#{name := Other, salt_position := SaltPosition} = Algorithm, Password) ->
Salt = gen_salt(Algorithm), Salt = gen_salt(Algorithm),
Hash = emqx_passwd:hash({Other, Salt, SaltPosition}, Password), Hash = emqx_passwd:hash({Other, Salt, SaltPosition}, Password),
@ -122,7 +143,12 @@ hash(#{name := Other, salt_position := SaltPosition} = Algorithm, Password) ->
emqx_passwd:password()) -> boolean()). emqx_passwd:password()) -> boolean()).
check_password(#{name := bcrypt}, _Salt, PasswordHash, Password) -> check_password(#{name := bcrypt}, _Salt, PasswordHash, Password) ->
emqx_passwd:check_pass({bcrypt, PasswordHash}, 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) -> check_password(#{name := Other, salt_position := SaltPosition}, Salt, PasswordHash, Password) ->
emqx_passwd:check_pass({Other, Salt, SaltPosition}, PasswordHash, Password). emqx_passwd:check_pass({Other, Salt, SaltPosition}, PasswordHash, Password).
@ -132,8 +158,10 @@ check_password(#{name := Other, salt_position := SaltPosition}, Salt, PasswordHa
rw_refs() -> rw_refs() ->
[hoconsc:ref(?MODULE, bcrypt_rw), [hoconsc:ref(?MODULE, bcrypt_rw),
hoconsc:ref(?MODULE, pbkdf2),
hoconsc:ref(?MODULE, other_algorithms)]. hoconsc:ref(?MODULE, other_algorithms)].
ro_refs() -> ro_refs() ->
[hoconsc:ref(?MODULE, bcrypt), [hoconsc:ref(?MODULE, bcrypt),
hoconsc:ref(?MODULE, pbkdf2),
hoconsc:ref(?MODULE, other_algorithms)]. hoconsc:ref(?MODULE, other_algorithms)].

View File

@ -116,9 +116,8 @@ hash_examples() ->
salt_position => prefix} salt_position => prefix}
}, },
#{ #{
password_hash => iolist_to_binary( password_hash => <<"a1509ab67bfacbad020927b5ac9d91e9100a82e33a0ebb01459367ce921c0aa8"
[<<"a1509ab67bfacbad020927b5ac9d91e9100a82e33a0ebb01459367ce921c0aa8">>, "157aa5652f94bc84fa3babc08283e44887d61c48bcf8ad7bcb3259ee7d0eafcd">>,
<<"157aa5652f94bc84fa3babc08283e44887d61c48bcf8ad7bcb3259ee7d0eafcd">>]),
salt => <<"salt">>, salt => <<"salt">>,
password => <<"sha512">>, password => <<"sha512">>,
password_hash_algorithm => #{name => sha512, password_hash_algorithm => #{name => sha512,
@ -131,5 +130,26 @@ hash_examples() ->
password_hash_algorithm => #{name => bcrypt, password_hash_algorithm => #{name => bcrypt,
salt_rounds => 10} salt_rounds => 10}
},
#{
password_hash => <<"01dbee7f4a9e243e988b62c73cda935d"
"a05378b93244ec8f48a99e61ad799d86">>,
salt => <<"ATHENA.MIT.EDUraeburn">>,
password => <<"password">>,
password_hash_algorithm => #{name => pbkdf2,
iterations => 2,
dk_length => 32,
mac_fun => sha}
},
#{
password_hash => <<"01dbee7f4a9e243e988b62c73cda935da05378b9">>,
salt => <<"ATHENA.MIT.EDUraeburn">>,
password => <<"password">>,
password_hash_algorithm => #{name => pbkdf2,
iterations => 2,
mac_fun => sha}
} }
]. ].

View File

@ -222,28 +222,28 @@ raw_redis_auth_config() ->
user_seeds() -> user_seeds() ->
[#{data => #{ [#{data => #{
password_hash => "plainsalt", password_hash => <<"plainsalt">>,
salt => "salt", salt => <<"salt">>,
is_superuser => "1" is_superuser => <<"1">>
}, },
credentials => #{ credentials => #{
username => <<"plain">>, username => <<"plain">>,
password => <<"plain">>}, password => <<"plain">>},
key => "mqtt_user:plain", key => <<"mqtt_user:plain">>,
config_params => #{}, config_params => #{},
result => {ok,#{is_superuser => true}} result => {ok,#{is_superuser => true}}
}, },
#{data => #{ #{data => #{
password_hash => "9b4d0c43d206d48279e69b9ad7132e22", password_hash => <<"9b4d0c43d206d48279e69b9ad7132e22">>,
salt => "salt", salt => <<"salt">>,
is_superuser => "0" is_superuser => <<"0">>
}, },
credentials => #{ credentials => #{
username => <<"md5">>, username => <<"md5">>,
password => <<"md5">> password => <<"md5">>
}, },
key => "mqtt_user:md5", key => <<"mqtt_user:md5">>,
config_params => #{ config_params => #{
password_hash_algorithm => #{name => <<"md5">>, password_hash_algorithm => #{name => <<"md5">>,
salt_position => <<"suffix">>} salt_position => <<"suffix">>}
@ -252,15 +252,15 @@ user_seeds() ->
}, },
#{data => #{ #{data => #{
password_hash => "ac63a624e7074776d677dd61a003b8c803eb11db004d0ec6ae032a5d7c9c5caf", password_hash => <<"ac63a624e7074776d677dd61a003b8c803eb11db004d0ec6ae032a5d7c9c5caf">>,
salt => "salt", salt => <<"salt">>,
is_superuser => "1" is_superuser => <<"1">>
}, },
credentials => #{ credentials => #{
clientid => <<"sha256">>, clientid => <<"sha256">>,
password => <<"sha256">> password => <<"sha256">>
}, },
key => "mqtt_user:sha256", key => <<"mqtt_user:sha256">>,
config_params => #{ config_params => #{
cmd => <<"HMGET mqtt_user:${clientid} password_hash salt is_superuser">>, cmd => <<"HMGET mqtt_user:${clientid} password_hash salt is_superuser">>,
password_hash_algorithm => #{name => <<"sha256">>, password_hash_algorithm => #{name => <<"sha256">>,
@ -270,31 +270,48 @@ user_seeds() ->
}, },
#{data => #{ #{data => #{
password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u", password_hash => <<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>,
salt => "$2b$12$wtY3h20mUjjmeaClpqZVve", salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>,
is_superuser => "0" is_superuser => <<"0">>
}, },
credentials => #{ credentials => #{
username => <<"bcrypt">>, username => <<"bcrypt">>,
password => <<"bcrypt">> password => <<"bcrypt">>
}, },
key => "mqtt_user:bcrypt", key => <<"mqtt_user:bcrypt">>,
config_params => #{ config_params => #{
password_hash_algorithm => #{name => <<"bcrypt">>} password_hash_algorithm => #{name => <<"bcrypt">>}
}, },
result => {ok,#{is_superuser => false}} result => {ok,#{is_superuser => false}}
}, },
#{data => #{ #{data => #{
password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u", password_hash => <<"01dbee7f4a9e243e988b62c73cda935da05378b9">>,
salt => "$2b$12$wtY3h20mUjjmeaClpqZVve", salt => <<"ATHENA.MIT.EDUraeburn">>,
is_superuser => "0" 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 => #{ credentials => #{
username => <<"bcrypt0">>, username => <<"bcrypt0">>,
password => <<"bcrypt">> password => <<"bcrypt">>
}, },
key => "mqtt_user:bcrypt0", key => <<"mqtt_user:bcrypt0">>,
config_params => #{ config_params => #{
% clientid variable & username credentials % clientid variable & username credentials
cmd => <<"HMGET mqtt_client:${clientid} password_hash salt is_superuser">>, cmd => <<"HMGET mqtt_client:${clientid} password_hash salt is_superuser">>,
@ -304,15 +321,15 @@ user_seeds() ->
}, },
#{data => #{ #{data => #{
password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u", password_hash => <<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>,
salt => "$2b$12$wtY3h20mUjjmeaClpqZVve", salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>,
is_superuser => "0" is_superuser => <<"0">>
}, },
credentials => #{ credentials => #{
username => <<"bcrypt1">>, username => <<"bcrypt1">>,
password => <<"bcrypt">> password => <<"bcrypt">>
}, },
key => "mqtt_user:bcrypt1", key => <<"mqtt_user:bcrypt1">>,
config_params => #{ config_params => #{
% Bad key in cmd % Bad key in cmd
cmd => <<"HMGET badkey:${username} password_hash salt is_superuser">>, cmd => <<"HMGET badkey:${username} password_hash salt is_superuser">>,
@ -322,16 +339,16 @@ user_seeds() ->
}, },
#{data => #{ #{data => #{
password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u", password_hash => <<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>,
salt => "$2b$12$wtY3h20mUjjmeaClpqZVve", salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>,
is_superuser => "0" is_superuser => <<"0">>
}, },
credentials => #{ credentials => #{
username => <<"bcrypt2">>, username => <<"bcrypt2">>,
% Wrong password % Wrong password
password => <<"wrongpass">> password => <<"wrongpass">>
}, },
key => "mqtt_user:bcrypt2", key => <<"mqtt_user:bcrypt2">>,
config_params => #{ config_params => #{
cmd => <<"HMGET mqtt_user:${username} password_hash salt is_superuser">>, cmd => <<"HMGET mqtt_user:${username} password_hash salt is_superuser">>,
password_hash_algorithm => #{name => <<"bcrypt">>} password_hash_algorithm => #{name => <<"bcrypt">>}