diff --git a/apps/emqx/src/emqx_passwd.erl b/apps/emqx/src/emqx_passwd.erl index f729ce49e..2f9775d6d 100644 --- a/apps/emqx/src/emqx_passwd.erl +++ b/apps/emqx/src/emqx_passwd.erl @@ -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(<>) -> - 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 e3c904a2e..066912ba1 100644 --- a/apps/emqx/test/emqx_passwd_SUITE.erl +++ b/apps/emqx/test/emqx_passwd_SUITE.erl @@ -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)). diff --git a/apps/emqx_authn/src/emqx_authn_password_hashing.erl b/apps/emqx_authn/src/emqx_authn_password_hashing.erl index 4cb1ad401..9e3637285 100644 --- a/apps/emqx_authn/src/emqx_authn_password_hashing.erl +++ b/apps/emqx_authn/src/emqx_authn_password_hashing.erl @@ -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)]. diff --git a/apps/emqx_authn/test/emqx_authn_password_hashing_SUITE.erl b/apps/emqx_authn/test/emqx_authn_password_hashing_SUITE.erl index e0273e24f..8832c551d 100644 --- a/apps/emqx_authn/test/emqx_authn_password_hashing_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_password_hashing_SUITE.erl @@ -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} } ]. diff --git a/apps/emqx_authn/test/emqx_authn_redis_SUITE.erl b/apps/emqx_authn/test/emqx_authn_redis_SUITE.erl index 938ca8714..de556a7bd 100644 --- a/apps/emqx_authn/test/emqx_authn_redis_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_redis_SUITE.erl @@ -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">>}