This is an automated email from the ASF dual-hosted git repository.

rnewson pushed a commit to branch decouple_offline_hash_strength_from_online
in repository https://gitbox.apache.org/repos/asf/couchdb.git

commit d2dc2f520265a64e537abf3ca3c1eceae56b7edf
Author: Robert Newson <[email protected]>
AuthorDate: Fri Oct 20 15:08:18 2023 +0100

    Introduce pbkdf2_prf parameter
    
    default to "sha" for existing credentials
    default to "sha256" when creating new credentials
---
 rel/overlay/etc/default.ini                    |  1 +
 src/couch/src/couch_auth_cache.erl             | 24 +++++++--
 src/couch/src/couch_httpd_auth.erl             | 17 +++++-
 src/couch/src/couch_passwords.erl              | 73 +++++++++++++++-----------
 src/couch/src/couch_users_db.erl               | 17 +++---
 src/couch/test/eunit/couch_passwords_tests.erl | 12 ++---
 src/docs/src/intro/security.rst                |  4 +-
 test/elixir/test/config_test.exs               |  2 +-
 8 files changed, 100 insertions(+), 50 deletions(-)

diff --git a/rel/overlay/etc/default.ini b/rel/overlay/etc/default.ini
index 1c94502b1..1604f1585 100644
--- a/rel/overlay/etc/default.ini
+++ b/rel/overlay/etc/default.ini
@@ -325,6 +325,7 @@ bind_address = 127.0.0.1
 ;min_iterations = 1
 ;max_iterations = 1000000000
 ;password_scheme = pbkdf2
+;pbkdf2_prf = sha256
 
 ; List of Erlang RegExp or tuples of RegExp and an optional error message.
 ; Where a new password must match all RegExp.
diff --git a/src/couch/src/couch_auth_cache.erl 
b/src/couch/src/couch_auth_cache.erl
index f361ab231..efa8e4765 100644
--- a/src/couch/src/couch_auth_cache.erl
+++ b/src/couch/src/couch_auth_cache.erl
@@ -70,15 +70,30 @@ get_admin(UserName) when is_list(UserName) ->
             % the name is an admin, now check to see if there is a user doc
             % which has a matching name, salt, and password_sha
             [HashedPwd, Salt] = string:tokens(HashedPwdAndSalt, ","),
-            make_admin_doc(HashedPwd, Salt);
+            make_admin_doc_simple(HashedPwd, Salt);
         "-pbkdf2-" ++ HashedPwdSaltAndIterations ->
             [HashedPwd, Salt, Iterations] = 
string:tokens(HashedPwdSaltAndIterations, ","),
-            make_admin_doc(HashedPwd, Salt, Iterations);
+            make_admin_doc_pbkdf2(<<"sha">>, HashedPwd, Salt, Iterations);
+        "-pbkdf2:sha-" ++ HashedPwdSaltAndIterations ->
+            [HashedPwd, Salt, Iterations] = 
string:tokens(HashedPwdSaltAndIterations, ","),
+            make_admin_doc_pbkdf2(<<"sha">>, HashedPwd, Salt, Iterations);
+        "-pbkdf2:sha224-" ++ HashedPwdSaltAndIterations ->
+            [HashedPwd, Salt, Iterations] = 
string:tokens(HashedPwdSaltAndIterations, ","),
+            make_admin_doc_pbkdf2(<<"sha224">>, HashedPwd, Salt, Iterations);
+        "-pbkdf2:sha256-" ++ HashedPwdSaltAndIterations ->
+            [HashedPwd, Salt, Iterations] = 
string:tokens(HashedPwdSaltAndIterations, ","),
+            make_admin_doc_pbkdf2(<<"sha256">>, HashedPwd, Salt, Iterations);
+        "-pbkdf2:sha384-" ++ HashedPwdSaltAndIterations ->
+            [HashedPwd, Salt, Iterations] = 
string:tokens(HashedPwdSaltAndIterations, ","),
+            make_admin_doc_pbkdf2(<<"sha384">>, HashedPwd, Salt, Iterations);
+        "-pbkdf2:sha512-" ++ HashedPwdSaltAndIterations ->
+            [HashedPwd, Salt, Iterations] = 
string:tokens(HashedPwdSaltAndIterations, ","),
+            make_admin_doc_pbkdf2(<<"sha512">>, HashedPwd, Salt, Iterations);
         _Else ->
             nil
     end.
 
-make_admin_doc(HashedPwd, Salt) ->
+make_admin_doc_simple(HashedPwd, Salt) ->
     [
         {<<"roles">>, [<<"_admin">>]},
         {<<"salt">>, ?l2b(Salt)},
@@ -86,12 +101,13 @@ make_admin_doc(HashedPwd, Salt) ->
         {<<"password_sha">>, ?l2b(HashedPwd)}
     ].
 
-make_admin_doc(DerivedKey, Salt, Iterations) ->
+make_admin_doc_pbkdf2(PRF, DerivedKey, Salt, Iterations) ->
     [
         {<<"roles">>, [<<"_admin">>]},
         {<<"salt">>, ?l2b(Salt)},
         {<<"iterations">>, list_to_integer(Iterations)},
         {<<"password_scheme">>, <<"pbkdf2">>},
+        {<<"pbkdf2_prf">>, PRF},
         {<<"derived_key">>, ?l2b(DerivedKey)}
     ].
 
diff --git a/src/couch/src/couch_httpd_auth.erl 
b/src/couch/src/couch_httpd_auth.erl
index 29cb58db7..6bb4774ce 100644
--- a/src/couch/src/couch_httpd_auth.erl
+++ b/src/couch/src/couch_httpd_auth.erl
@@ -662,10 +662,14 @@ authenticate(Pass, UserProps) ->
                     couch_util:get_value(<<"password_sha">>, UserProps, nil)
                 };
             <<"pbkdf2">> ->
+                PRF = couch_util:get_value(<<"pbkdf2_prf">>, UserProps, 
<<"sha">>),
+                verify_prf(PRF),
                 Iterations = couch_util:get_value(<<"iterations">>, UserProps, 
10000),
                 verify_iterations(Iterations),
                 {
-                    couch_passwords:pbkdf2(Pass, UserSalt, Iterations),
+                    couch_passwords:pbkdf2(
+                        binary_to_existing_atom(PRF), Pass, UserSalt, 
Iterations
+                    ),
                     couch_util:get_value(<<"derived_key">>, UserProps, nil)
                 }
         end,
@@ -687,6 +691,17 @@ verify_iterations(Iterations) when is_integer(Iterations) 
->
             ok
     end.
 
+verify_prf(PRF) when
+    PRF == <<"sha">>;
+    PRF == <<"sha224">>;
+    PRF == <<"sha256">>;
+    PRF == <<"sha384">>;
+    PRF == <<"sha512">>
+->
+    ok;
+verify_prf(_PRF) ->
+    throw({forbidden, <<"PRF is invalid">>}).
+
 make_cookie_time() ->
     {NowMS, NowS, _} = os:timestamp(),
     NowMS * 1000000 + NowS.
diff --git a/src/couch/src/couch_passwords.erl 
b/src/couch/src/couch_passwords.erl
index ac8990861..a26d7fbd9 100644
--- a/src/couch/src/couch_passwords.erl
+++ b/src/couch/src/couch_passwords.erl
@@ -12,14 +12,11 @@
 
 -module(couch_passwords).
 
--export([simple/2, pbkdf2/3, pbkdf2/4, verify/2]).
+-export([simple/2, pbkdf2/3, pbkdf2/4, pbkdf2/5, verify/2]).
 -export([hash_admin_password/1, get_unhashed_admins/0]).
 
 -include_lib("couch/include/couch_db.hrl").
 
--define(MAX_DERIVED_KEY_LENGTH, (1 bsl 32 - 1)).
--define(SHA1_OUTPUT_LENGTH, 20).
-
 %% legacy scheme, not used for new passwords.
 -spec simple(binary(), binary()) -> binary().
 simple(Password, Salt) when is_binary(Password), is_binary(Salt) ->
@@ -46,15 +43,17 @@ hash_admin_password("simple", ClearPassword) ->
     Hash = crypto:hash(sha, <<ClearPassword/binary, Salt/binary>>),
     ?l2b("-hashed-" ++ couch_util:to_hex(Hash) ++ "," ++ ?b2l(Salt));
 hash_admin_password("pbkdf2", ClearPassword) ->
+    PRF = chttpd_util:get_chttpd_auth_config("pbkdf2_prf", "sha256"),
     Iterations = chttpd_util:get_chttpd_auth_config("iterations", "10"),
     Salt = couch_uuids:random(),
     DerivedKey = couch_passwords:pbkdf2(
+        list_to_existing_atom(PRF),
         couch_util:to_binary(ClearPassword),
         Salt,
         list_to_integer(Iterations)
     ),
     ?l2b(
-        "-pbkdf2-" ++ ?b2l(DerivedKey) ++ "," ++
+        "-pbkdf2:" ++ PRF ++ "-" ++ ?b2l(DerivedKey) ++ "," ++
             ?b2l(Salt) ++ "," ++
             Iterations
     ).
@@ -69,53 +68,65 @@ get_unhashed_admins() ->
             ({_User, "-pbkdf2-" ++ _}) ->
                 % already hashed
                 false;
+            ({_User, "-pbkdf2:sha-" ++ _}) ->
+                % already hashed
+                false;
+            ({_User, "-pbkdf2:sha224-" ++ _}) ->
+                % already hashed
+                false;
+            ({_User, "-pbkdf2:sha256-" ++ _}) ->
+                % already hashed
+                false;
+            ({_User, "-pbkdf2:sha384-" ++ _}) ->
+                % already hashed
+                false;
+            ({_User, "-pbkdf2:sha512-" ++ _}) ->
+                % already hashed
+                false;
             ({_User, _ClearPassword}) ->
                 true
         end,
         config:get("admins")
     ).
 
-%% Current scheme, much stronger.
--spec pbkdf2(binary(), binary(), integer()) -> binary().
-pbkdf2(Password, Salt, Iterations) when
+pbkdf2(Password, Salt, Iterations) ->
+    pbkdf2(sha, Password, Salt, Iterations).
+
+pbkdf2(PRF, Password, Salt, Iterations) when is_atom(PRF) ->
+    #{size := Size} = crypto:hash_info(PRF),
+    pbkdf2(PRF, Password, Salt, Iterations, Size);
+pbkdf2(Password, Salt, Iterations, KeyLen) ->
+    pbkdf2(sha, Password, Salt, Iterations, KeyLen).
+
+pbkdf2(PRF, Password, Salt, Iterations, KeyLen) when
+    is_atom(PRF),
     is_binary(Password),
     is_binary(Salt),
     is_integer(Iterations),
-    Iterations > 0
+    Iterations > 0,
+    KeyLen > 0
 ->
-    {ok, Result} = pbkdf2(Password, Salt, Iterations, ?SHA1_OUTPUT_LENGTH),
-    Result;
-pbkdf2(Password, Salt, Iterations) when
+    DerivedKey = fast_pbkdf2:pbkdf2(PRF, Password, Salt, Iterations, KeyLen),
+    couch_util:to_hex_bin(DerivedKey);
+pbkdf2(PRF, Password, Salt, Iterations, KeyLen) when
+    is_atom(PRF),
     is_binary(Salt),
     is_integer(Iterations),
-    Iterations > 0
+    Iterations > 0,
+    KeyLen > 0
 ->
     Msg = io_lib:format("Password value of '~p' is invalid.", [Password]),
     throw({forbidden, Msg});
-pbkdf2(Password, Salt, Iterations) when
+pbkdf2(PRF, Password, Salt, Iterations, KeyLen) when
+    is_atom(PRF),
     is_binary(Password),
     is_integer(Iterations),
-    Iterations > 0
+    Iterations > 0,
+    KeyLen > 0
 ->
     Msg = io_lib:format("Salt value of '~p' is invalid.", [Salt]),
     throw({forbidden, Msg}).
 
--spec pbkdf2(binary(), binary(), integer(), integer()) ->
-    {ok, binary()} | {error, derived_key_too_long}.
-pbkdf2(_Password, _Salt, _Iterations, DerivedLength) when
-    DerivedLength > ?MAX_DERIVED_KEY_LENGTH
-->
-    {error, derived_key_too_long};
-pbkdf2(Password, Salt, Iterations, DerivedLength) when
-    is_binary(Password),
-    is_binary(Salt),
-    is_integer(Iterations),
-    Iterations > 0,
-    is_integer(DerivedLength)
-->
-    DerivedKey = fast_pbkdf2:pbkdf2(sha, Password, Salt, Iterations, 
DerivedLength),
-    {ok, couch_util:to_hex_bin(DerivedKey)}.
-
 %% verify two lists for equality without short-circuits to avoid timing 
attacks.
 -if((?OTP_RELEASE) >= 25).
 verify(BinA, BinB) when is_binary(BinA), is_binary(BinB), byte_size(BinA) == 
byte_size(BinB) ->
diff --git a/src/couch/src/couch_users_db.erl b/src/couch/src/couch_users_db.erl
index 7ef3aee78..f8d56882a 100644
--- a/src/couch/src/couch_users_db.erl
+++ b/src/couch/src/couch_users_db.erl
@@ -23,6 +23,7 @@
 -define(SIMPLE, <<"simple">>).
 -define(PASSWORD_SHA, <<"password_sha">>).
 -define(PBKDF2, <<"pbkdf2">>).
+-define(PBKDF2_PRF, <<"pbkdf2_prf">>).
 -define(ITERATIONS, <<"iterations">>).
 -define(SALT, <<"salt">>).
 -define(replace(L, K, V), lists:keystore(K, 1, L, {K, V})).
@@ -81,17 +82,21 @@ save_doc(#doc{body = {Body}} = Doc) ->
             Doc#doc{body = {Body3}};
         {ClearPassword, "pbkdf2"} ->
             ok = validate_password(ClearPassword),
+            PRF = chttpd_util:get_chttpd_auth_config("pbkdf2_prf", "sha256"),
             Iterations = chttpd_util:get_chttpd_auth_config_integer(
                 "iterations", 10
             ),
             Salt = couch_uuids:random(),
-            DerivedKey = couch_passwords:pbkdf2(ClearPassword, Salt, 
Iterations),
+            DerivedKey = couch_passwords:pbkdf2(
+                list_to_existing_atom(PRF), ClearPassword, Salt, Iterations
+            ),
             Body0 = ?replace(Body, ?PASSWORD_SCHEME, ?PBKDF2),
-            Body1 = ?replace(Body0, ?ITERATIONS, Iterations),
-            Body2 = ?replace(Body1, ?DERIVED_KEY, DerivedKey),
-            Body3 = ?replace(Body2, ?SALT, Salt),
-            Body4 = proplists:delete(?PASSWORD, Body3),
-            Doc#doc{body = {Body4}};
+            Body1 = ?replace(Body0, ?PBKDF2_PRF, ?l2b(PRF)),
+            Body2 = ?replace(Body1, ?ITERATIONS, Iterations),
+            Body3 = ?replace(Body2, ?DERIVED_KEY, DerivedKey),
+            Body4 = ?replace(Body3, ?SALT, Salt),
+            Body5 = proplists:delete(?PASSWORD, Body4),
+            Doc#doc{body = {Body5}};
         {_ClearPassword, Scheme} ->
             couch_log:error("[couch_httpd_auth] password_scheme value of '~p' 
is invalid.", [Scheme]),
             throw({forbidden, ?PASSWORD_SERVER_ERROR})
diff --git a/src/couch/test/eunit/couch_passwords_tests.erl 
b/src/couch/test/eunit/couch_passwords_tests.erl
index 6b67a99e3..a702f00c6 100644
--- a/src/couch/test/eunit/couch_passwords_tests.erl
+++ b/src/couch/test/eunit/couch_passwords_tests.erl
@@ -18,25 +18,25 @@ pbkdf2_test_() ->
     {"PBKDF2", [
         {"Iterations: 1, length: 20",
             ?_assertEqual(
-                {ok, <<"0c60c80f961f0e71f3a9b524af6012062fe037a6">>},
+                <<"0c60c80f961f0e71f3a9b524af6012062fe037a6">>,
                 couch_passwords:pbkdf2(<<"password">>, <<"salt">>, 1, 20)
             )},
 
         {"Iterations: 2, length: 20",
             ?_assertEqual(
-                {ok, <<"ea6c014dc72d6f8ccd1ed92ace1d41f0d8de8957">>},
+                <<"ea6c014dc72d6f8ccd1ed92ace1d41f0d8de8957">>,
                 couch_passwords:pbkdf2(<<"password">>, <<"salt">>, 2, 20)
             )},
 
         {"Iterations: 4096, length: 20",
             ?_assertEqual(
-                {ok, <<"4b007901b765489abead49d926f721d065a429c1">>},
+                <<"4b007901b765489abead49d926f721d065a429c1">>,
                 couch_passwords:pbkdf2(<<"password">>, <<"salt">>, 4096, 20)
             )},
 
         {"Iterations: 4096, length: 25",
             ?_assertEqual(
-                {ok, <<"3d2eec4fe41c849b80c8d83662c0e44a8b291a964cf2f07038">>},
+                <<"3d2eec4fe41c849b80c8d83662c0e44a8b291a964cf2f07038">>,
                 couch_passwords:pbkdf2(
                     <<"passwordPASSWORDpassword">>,
                     <<"saltSALTsaltSALTsaltSALTsaltSALTsalt">>,
@@ -46,7 +46,7 @@ pbkdf2_test_() ->
             )},
         {"Null byte",
             ?_assertEqual(
-                {ok, <<"56fa6aa75548099dcc37d7f03425e0c3">>},
+                <<"56fa6aa75548099dcc37d7f03425e0c3">>,
                 couch_passwords:pbkdf2(
                     <<"pass\0word">>,
                     <<"sa\0lt">>,
@@ -59,7 +59,7 @@ pbkdf2_test_() ->
         {timeout, 600,
             {"Iterations: 16777216 - this may take some time",
                 ?_assertEqual(
-                    {ok, <<"eefe3d61cd4da4e4e9945b3d6ba2158c2634e984">>},
+                    <<"eefe3d61cd4da4e4e9945b3d6ba2158c2634e984">>,
                     couch_passwords:pbkdf2(<<"password">>, <<"salt">>, 
16777216, 20)
                 )}}
     ]}.
diff --git a/src/docs/src/intro/security.rst b/src/docs/src/intro/security.rst
index a9cbfc32d..708b91925 100644
--- a/src/docs/src/intro/security.rst
+++ b/src/docs/src/intro/security.rst
@@ -288,7 +288,7 @@ several *mandatory* fields, that CouchDB needs for 
authentication:
 
 - **_id** (*string*): Document ID. Contains user's login with special prefix
   :ref:`org.couchdb.user`
-- **derived_key** (*string*): `PBKDF2`_ key derived from salt/iterations.
+- **derived_key** (*string*): `PBKDF2`_ key derived from prf/salt/iterations.
 - **name** (*string*): User's name aka login. **Immutable** e.g. you cannot
   rename an existing user - you have to create new one
 - **roles** (*array* of *string*): List of user roles. CouchDB doesn't provide
@@ -305,6 +305,8 @@ several *mandatory* fields, that CouchDB needs for 
authentication:
   ``password_scheme`` options.
 - **iterations** (*integer*): Number of iterations to derive key, used for 
``pbkdf2``
   ``password_scheme`` See the :ref:`configuration API <config/chttpd_auth>`:: 
for details.
+  **pbkdf2_prf** (*string*): The PRF to use for ``pbkdf2``. If missing, 
``sha`` is
+  assumed. Can be any of ``sha``, ``sha224``, ``sha256``, ``sha384``, 
``sha512``.
 - **type** (*string*): Document type. Constantly has the value ``user``
 
 Additionally, you may specify any custom fields that relate to the target
diff --git a/test/elixir/test/config_test.exs b/test/elixir/test/config_test.exs
index d2d72cab8..ac7ec93e3 100644
--- a/test/elixir/test/config_test.exs
+++ b/test/elixir/test/config_test.exs
@@ -92,7 +92,7 @@ defmodule ConfigTest do
     assert Couch.login("administrator", plain_pass)
     hash_pass = get_config(context, "admins", "administrator")
 
-    assert Regex.match?(~r/^-pbkdf2-/, hash_pass) or
+    assert Regex.match?(~r/^-pbkdf2(:[a-z0-9]+)?-/, hash_pass) or
              Regex.match?(~r/^-hashed-/, hash_pass)
 
     delete_config(context, "admins", "administrator")

Reply via email to