refactor(authn): restore pbkdf2 password hashing functionality
This commit is contained in:
parent
2b0a3e8ba3
commit
708d9cfc6c
|
@ -34,18 +34,34 @@
|
|||
-type(password_hash() :: binary()).
|
||||
|
||||
-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() :: 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
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-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) ->
|
||||
case bcrypt:hashpw(Password, Salt) of
|
||||
{ok, HashPasswd} ->
|
||||
|
@ -58,6 +74,13 @@ check_pass({_SimpleHash, _Salt, _SaltPosition} = HashParams, PasswordHash, Passw
|
|||
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) ->
|
||||
case bcrypt:hashpw(Password, Salt) of
|
||||
{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) ->
|
||||
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) ->
|
||||
hexstring(crypto:hash(sha, Data));
|
||||
hex(crypto:hash(sha, 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) ->
|
||||
hexstring(crypto:hash(sha512, Data)).
|
||||
hex(crypto:hash(sha512, Data)).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Internal functions
|
||||
|
@ -103,11 +126,11 @@ compare_secure([], [], Result) ->
|
|||
Result == 0.
|
||||
|
||||
|
||||
hexstring(<<X:128/big-unsigned-integer>>) ->
|
||||
iolist_to_binary(io_lib:format("~32.16.0b", [X]));
|
||||
hexstring(<<X:160/big-unsigned-integer>>) ->
|
||||
iolist_to_binary(io_lib:format("~40.16.0b", [X]));
|
||||
hexstring(<<X:256/big-unsigned-integer>>) ->
|
||||
iolist_to_binary(io_lib:format("~64.16.0b", [X]));
|
||||
hexstring(<<X:512/big-unsigned-integer>>) ->
|
||||
iolist_to_binary(io_lib:format("~128.16.0b", [X])).
|
||||
pbkdf2(MacFun, Password, Salt, Iterations, undefined) ->
|
||||
pbkdf2:pbkdf2(MacFun, Password, Salt, Iterations);
|
||||
pbkdf2(MacFun, Password, Salt, Iterations, DKLength) ->
|
||||
pbkdf2:pbkdf2(MacFun, Password, Salt, Iterations, DKLength).
|
||||
|
||||
|
||||
hex(X) when is_binary(X) ->
|
||||
pbkdf2:to_hex(X).
|
||||
|
|
|
@ -88,4 +88,16 @@ t_hash(_) ->
|
|||
false = emqx_passwd:check_pass({bcrypt, <<>>}, <<>>, WrongPassword),
|
||||
|
||||
%% 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)).
|
||||
|
|
|
@ -27,8 +27,12 @@
|
|||
-type(bcrypt_algorithm() :: #{name := bcrypt}).
|
||||
-type(bcrypt_algorithm_rw() :: #{name := bcrypt, salt_rounds := integer()}).
|
||||
|
||||
-type(algorithm() :: simple_algorithm() | bcrypt_algorithm()).
|
||||
-type(algorithm_rw() :: simple_algorithm() | bcrypt_algorithm_rw()).
|
||||
-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
|
||||
|
@ -47,7 +51,7 @@
|
|||
hash/2,
|
||||
check_password/4]).
|
||||
|
||||
roots() -> [bcrypt, bcrypt_rw, other_algorithms].
|
||||
roots() -> [pbkdf2, bcrypt, bcrypt_rw, other_algorithms].
|
||||
|
||||
fields(bcrypt_rw) ->
|
||||
fields(bcrypt) ++
|
||||
|
@ -56,6 +60,12 @@ fields(bcrypt_rw) ->
|
|||
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}].
|
||||
|
@ -68,6 +78,11 @@ 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};
|
||||
|
@ -108,7 +123,13 @@ hash(#{name := bcrypt, salt_rounds := _} = Algorithm, Password) ->
|
|||
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),
|
||||
|
@ -122,7 +143,12 @@ hash(#{name := Other, salt_position := SaltPosition} = Algorithm, Password) ->
|
|||
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).
|
||||
|
||||
|
@ -132,8 +158,10 @@ check_password(#{name := Other, salt_position := SaltPosition}, Salt, PasswordHa
|
|||
|
||||
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)].
|
||||
|
|
|
@ -116,9 +116,8 @@ hash_examples() ->
|
|||
salt_position => prefix}
|
||||
},
|
||||
#{
|
||||
password_hash => iolist_to_binary(
|
||||
[<<"a1509ab67bfacbad020927b5ac9d91e9100a82e33a0ebb01459367ce921c0aa8">>,
|
||||
<<"157aa5652f94bc84fa3babc08283e44887d61c48bcf8ad7bcb3259ee7d0eafcd">>]),
|
||||
password_hash => <<"a1509ab67bfacbad020927b5ac9d91e9100a82e33a0ebb01459367ce921c0aa8"
|
||||
"157aa5652f94bc84fa3babc08283e44887d61c48bcf8ad7bcb3259ee7d0eafcd">>,
|
||||
salt => <<"salt">>,
|
||||
password => <<"sha512">>,
|
||||
password_hash_algorithm => #{name => sha512,
|
||||
|
@ -131,5 +130,26 @@ hash_examples() ->
|
|||
|
||||
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}
|
||||
}
|
||||
].
|
||||
|
|
|
@ -222,28 +222,28 @@ raw_redis_auth_config() ->
|
|||
|
||||
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 => #{name => <<"md5">>,
|
||||
salt_position => <<"suffix">>}
|
||||
|
@ -252,15 +252,15 @@ user_seeds() ->
|
|||
},
|
||||
|
||||
#{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 => #{name => <<"sha256">>,
|
||||
|
@ -270,31 +270,48 @@ user_seeds() ->
|
|||
},
|
||||
|
||||
#{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 => #{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">>,
|
||||
|
@ -304,15 +321,15 @@ user_seeds() ->
|
|||
},
|
||||
|
||||
#{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">>,
|
||||
|
@ -322,16 +339,16 @@ user_seeds() ->
|
|||
},
|
||||
|
||||
#{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 => #{name => <<"bcrypt">>}
|
||||
|
|
Loading…
Reference in New Issue