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

refactor(authn): restore pbkdf2 password hashing functionality

Ilya Averyanov 4 лет назад
Родитель
Сommit
708d9cfc6c

+ 37 - 14
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(<<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).

+ 13 - 1
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)).

+ 33 - 5
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)].

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

+ 46 - 29
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">>}