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 c0be71dd8f50aca0d22c67f39985cb4c66cbb388
Author: Robert Newson <[email protected]>
AuthorDate: Wed Oct 18 18:18:51 2023 +0100

    introduce pbkdf2_sha256 password scheme as new default
---
 rel/overlay/etc/default.ini                    |  2 +-
 src/couch/include/couch_js_functions.hrl       |  5 +-
 src/couch/src/couch_auth_cache.erl             | 20 ++++++--
 src/couch/src/couch_httpd_auth.erl             |  7 +++
 src/couch/src/couch_passwords.erl              | 66 ++++++++++++++++++++++++--
 src/couch/src/couch_users_db.erl               | 18 ++++++-
 src/couch/test/eunit/couch_passwords_tests.erl | 23 +++++++++
 src/docs/src/intro/security.rst                |  9 ++--
 test/elixir/test/config_test.exs               |  3 +-
 9 files changed, 135 insertions(+), 18 deletions(-)

diff --git a/rel/overlay/etc/default.ini b/rel/overlay/etc/default.ini
index 1c94502b1..2c28c4d79 100644
--- a/rel/overlay/etc/default.ini
+++ b/rel/overlay/etc/default.ini
@@ -324,7 +324,7 @@ bind_address = 127.0.0.1
 ;iterations = 10 ; iterations for password hashing
 ;min_iterations = 1
 ;max_iterations = 1000000000
-;password_scheme = pbkdf2
+;password_scheme = pbkdf2_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/include/couch_js_functions.hrl 
b/src/couch/include/couch_js_functions.hrl
index 737b71c7f..66d684299 100644
--- a/src/couch/include/couch_js_functions.hrl
+++ b/src/couch/include/couch_js_functions.hrl
@@ -64,7 +64,7 @@
             });
         }
 
-        var available_schemes = [\"simple\", \"pbkdf2\"];
+        var available_schemes = [\"simple\", \"pbkdf2\", \"pbkdf2_sha256\"];
         if (newDoc.password_scheme
                 && available_schemes.indexOf(newDoc.password_scheme) == -1) {
             throw({
@@ -73,7 +73,8 @@
             });
         }
 
-        if (newDoc.password_scheme === \"pbkdf2\") {
+        if (newDoc.password_scheme === \"pbkdf2\" ||
+            newDoc.password_scheme === \"pbkdf2_sha256\") {
             if (typeof(newDoc.iterations) !== \"number\") {
                throw({forbidden: \"iterations must be a number.\"});
             }
diff --git a/src/couch/src/couch_auth_cache.erl 
b/src/couch/src/couch_auth_cache.erl
index f361ab231..22686dfe9 100644
--- a/src/couch/src/couch_auth_cache.erl
+++ b/src/couch/src/couch_auth_cache.erl
@@ -70,15 +70,18 @@ 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(HashedPwd, Salt, Iterations);
+        "-pbkdf2_sha256-" ++ HashedPwdSaltAndIterations ->
+            [HashedPwd, Salt, Iterations] = 
string:tokens(HashedPwdSaltAndIterations, ","),
+            make_admin_doc_pbkdf2_sha256(HashedPwd, Salt, Iterations);
         _Else ->
             nil
     end.
 
-make_admin_doc(HashedPwd, Salt) ->
+make_admin_doc_simple(HashedPwd, Salt) ->
     [
         {<<"roles">>, [<<"_admin">>]},
         {<<"salt">>, ?l2b(Salt)},
@@ -86,7 +89,7 @@ make_admin_doc(HashedPwd, Salt) ->
         {<<"password_sha">>, ?l2b(HashedPwd)}
     ].
 
-make_admin_doc(DerivedKey, Salt, Iterations) ->
+make_admin_doc_pbkdf2(DerivedKey, Salt, Iterations) ->
     [
         {<<"roles">>, [<<"_admin">>]},
         {<<"salt">>, ?l2b(Salt)},
@@ -95,6 +98,15 @@ make_admin_doc(DerivedKey, Salt, Iterations) ->
         {<<"derived_key">>, ?l2b(DerivedKey)}
     ].
 
+make_admin_doc_pbkdf2_sha256(DerivedKey, Salt, Iterations) ->
+    [
+        {<<"roles">>, [<<"_admin">>]},
+        {<<"salt">>, ?l2b(Salt)},
+        {<<"iterations">>, list_to_integer(Iterations)},
+        {<<"password_scheme">>, <<"pbkdf2_sha256">>},
+        {<<"derived_key">>, ?l2b(DerivedKey)}
+    ].
+
 get_from_db(UserName) ->
     ok = ensure_users_db_exists(),
     couch_util:with_db(users_db(), fun(Db) ->
diff --git a/src/couch/src/couch_httpd_auth.erl 
b/src/couch/src/couch_httpd_auth.erl
index 29cb58db7..dc8771724 100644
--- a/src/couch/src/couch_httpd_auth.erl
+++ b/src/couch/src/couch_httpd_auth.erl
@@ -667,6 +667,13 @@ authenticate(Pass, UserProps) ->
                 {
                     couch_passwords:pbkdf2(Pass, UserSalt, Iterations),
                     couch_util:get_value(<<"derived_key">>, UserProps, nil)
+                };
+            <<"pbkdf2_sha256">> ->
+                Iterations = couch_util:get_value(<<"iterations">>, UserProps, 
50000),
+                verify_iterations(Iterations),
+                {
+                    couch_passwords:pbkdf2_sha256(Pass, UserSalt, Iterations),
+                    couch_util:get_value(<<"derived_key">>, UserProps, nil)
                 }
         end,
     couch_passwords:verify(PasswordHash, ExpectedHash).
diff --git a/src/couch/src/couch_passwords.erl 
b/src/couch/src/couch_passwords.erl
index 37f8c241e..d438524f1 100644
--- a/src/couch/src/couch_passwords.erl
+++ b/src/couch/src/couch_passwords.erl
@@ -12,13 +12,14 @@
 
 -module(couch_passwords).
 
--export([simple/2, pbkdf2/3, pbkdf2/4, verify/2]).
+-export([simple/2, pbkdf2/3, pbkdf2/4, pbkdf2_sha256/3, pbkdf2_sha256/4, 
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).
+-define(SHA2_OUTPUT_LENGTH, 32).
 
 %% legacy scheme, not used for new passwords.
 -spec simple(binary(), binary()) -> binary().
@@ -36,8 +37,8 @@ simple(Password, Salt) when is_binary(Password) ->
 hash_admin_password(ClearPassword) when is_list(ClearPassword) ->
     hash_admin_password(?l2b(ClearPassword));
 hash_admin_password(ClearPassword) when is_binary(ClearPassword) ->
-    %% Support both schemes to smooth migration from legacy scheme
-    Scheme = chttpd_util:get_chttpd_auth_config("password_scheme", "pbkdf2"),
+    %% Support all schemes to smooth migration from legacy scheme
+    Scheme = chttpd_util:get_chttpd_auth_config("password_scheme", 
"pbkdf2_sha256"),
     hash_admin_password(Scheme, ClearPassword).
 
 % deprecated
@@ -57,6 +58,19 @@ hash_admin_password("pbkdf2", ClearPassword) ->
         "-pbkdf2-" ++ ?b2l(DerivedKey) ++ "," ++
             ?b2l(Salt) ++ "," ++
             Iterations
+    );
+hash_admin_password("pbkdf2_sha256", ClearPassword) ->
+    Iterations = chttpd_util:get_chttpd_auth_config("iterations", "50000"),
+    Salt = couch_uuids:random(),
+    DerivedKey = couch_passwords:pbkdf2_sha256(
+        couch_util:to_binary(ClearPassword),
+        Salt,
+        list_to_integer(Iterations)
+    ),
+    ?l2b(
+        "-pbkdf2_sha256-" ++ ?b2l(DerivedKey) ++ "," ++
+            ?b2l(Salt) ++ "," ++
+            Iterations
     ).
 
 -spec get_unhashed_admins() -> list().
@@ -69,13 +83,16 @@ get_unhashed_admins() ->
             ({_User, "-pbkdf2-" ++ _}) ->
                 % already hashed
                 false;
+            ({_User, "-pbkdf2_sha256-" ++ _}) ->
+                % already hashed
+                false;
             ({_User, _ClearPassword}) ->
                 true
         end,
         config:get("admins")
     ).
 
-%% Current scheme, much stronger.
+%% PBKDF2 with SHA-1 scheme.
 -spec pbkdf2(binary(), binary(), integer()) -> binary().
 pbkdf2(Password, Salt, Iterations) when
     is_binary(Password),
@@ -116,6 +133,47 @@ pbkdf2(Password, Salt, Iterations, DerivedLength) when
     DerivedKey = crypto:pbkdf2_hmac(sha, Password, Salt, Iterations, 
DerivedLength),
     {ok, couch_util:to_hex_bin(DerivedKey)}.
 
+%% PBKDF2 with SHA256 scheme.
+-spec pbkdf2_sha256(binary(), binary(), integer()) -> binary().
+pbkdf2_sha256(Password, Salt, Iterations) when
+    is_binary(Password),
+    is_binary(Salt),
+    is_integer(Iterations),
+    Iterations > 0
+->
+    {ok, Result} = pbkdf2_sha256(Password, Salt, Iterations, 
?SHA2_OUTPUT_LENGTH),
+    Result;
+pbkdf2_sha256(Password, Salt, Iterations) when
+    is_binary(Salt),
+    is_integer(Iterations),
+    Iterations > 0
+->
+    Msg = io_lib:format("Password value of '~p' is invalid.", [Password]),
+    throw({forbidden, Msg});
+pbkdf2_sha256(Password, Salt, Iterations) when
+    is_binary(Password),
+    is_integer(Iterations),
+    Iterations > 0
+->
+    Msg = io_lib:format("Salt value of '~p' is invalid.", [Salt]),
+    throw({forbidden, Msg}).
+
+-spec pbkdf2_sha256(binary(), binary(), integer(), integer()) ->
+    {ok, binary()} | {error, derived_key_too_long}.
+pbkdf2_sha256(_Password, _Salt, _Iterations, DerivedLength) when
+    DerivedLength > ?MAX_DERIVED_KEY_LENGTH
+->
+    {error, derived_key_too_long};
+pbkdf2_sha256(Password, Salt, Iterations, DerivedLength) when
+    is_binary(Password),
+    is_binary(Salt),
+    is_integer(Iterations),
+    Iterations > 0,
+    is_integer(DerivedLength)
+->
+    DerivedKey = crypto:pbkdf2_hmac(sha256, Password, Salt, Iterations, 
DerivedLength),
+    {ok, couch_util:to_hex_bin(DerivedKey)}.
+
 -if((?OTP_RELEASE) >= 25).
 verify(BinA, BinB) ->
     crypto:hash_equals(BinA, BinB).
diff --git a/src/couch/src/couch_users_db.erl b/src/couch/src/couch_users_db.erl
index 7ef3aee78..0c24e792e 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_SHA256, <<"pbkdf2_sha256">>).
 -define(ITERATIONS, <<"iterations">>).
 -define(SALT, <<"salt">>).
 -define(replace(L, K, V), lists:keystore(K, 1, L, {K, V})).
@@ -61,8 +62,8 @@ before_doc_update(Doc, Db, _UpdateType) ->
 %    newDoc.salt = salt
 %    newDoc.password = null
 save_doc(#doc{body = {Body}} = Doc) ->
-    %% Support both schemes to smooth migration from legacy scheme
-    Scheme = chttpd_util:get_chttpd_auth_config("password_scheme", "pbkdf2"),
+    %% Support all schemes to smooth migration from legacy scheme
+    Scheme = chttpd_util:get_chttpd_auth_config("password_scheme", 
"pbkdf2_sha256"),
     case {couch_util:get_value(?PASSWORD, Body), Scheme} of
         % server admins don't have a user-db password entry
         {null, _} ->
@@ -92,6 +93,19 @@ save_doc(#doc{body = {Body}} = Doc) ->
             Body3 = ?replace(Body2, ?SALT, Salt),
             Body4 = proplists:delete(?PASSWORD, Body3),
             Doc#doc{body = {Body4}};
+        {ClearPassword, "pbkdf2_sha256"} ->
+            ok = validate_password(ClearPassword),
+            Iterations = chttpd_util:get_chttpd_auth_config_integer(
+                "iterations", 50000
+            ),
+            Salt = couch_uuids:random(),
+            DerivedKey = couch_passwords:pbkdf2_sha256(ClearPassword, Salt, 
Iterations),
+            Body0 = ?replace(Body, ?PASSWORD_SCHEME, ?PBKDF2_SHA256),
+            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}};
         {_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..f12116aeb 100644
--- a/src/couch/test/eunit/couch_passwords_tests.erl
+++ b/src/couch/test/eunit/couch_passwords_tests.erl
@@ -63,3 +63,26 @@ pbkdf2_test_() ->
                     couch_passwords:pbkdf2(<<"password">>, <<"salt">>, 
16777216, 20)
                 )}}
     ]}.
+
+pbkdf2_sha256_test_() ->
+    {"PBKDF2_SHA256", [
+        {"Iterations: 2, length: 20",
+            ?_assertEqual(
+                {ok, <<"ae4d0c95af6b46d32d0adff928f06dd02a303f8e">>},
+                couch_passwords:pbkdf2_sha256(<<"password">>, <<"salt">>, 2, 
20)
+            )},
+
+        {"Iterations: 4096, length: 20",
+            ?_assertEqual(
+                {ok, <<"c5e478d59288c841aa530db6845c4c8d962893a0">>},
+                couch_passwords:pbkdf2_sha256(<<"password">>, <<"salt">>, 
4096, 20)
+            )},
+
+        %% this may runs too long on slow hosts
+        {timeout, 600,
+            {"Iterations: 16777216 - this may take some time",
+                ?_assertEqual(
+                    {ok, <<"cf81c66fe8cfc04d1f31ecb65dab4089f7f179e8">>},
+                    couch_passwords:pbkdf2_sha256(<<"password">>, <<"salt">>, 
16777216, 20)
+                )}}
+    ]}.
diff --git a/src/docs/src/intro/security.rst b/src/docs/src/intro/security.rst
index a9cbfc32d..24d678397 100644
--- a/src/docs/src/intro/security.rst
+++ b/src/docs/src/intro/security.rst
@@ -299,12 +299,13 @@ several *mandatory* fields, that CouchDB needs for 
authentication:
   by hashed fields before the document is actually stored.
 - **password_sha** (*string*): Hashed password with salt. Used for ``simple``
   `password_scheme`
-- **password_scheme** (*string*): Password hashing scheme. May be ``simple`` or
-  ``pbkdf2``
-- **salt** (*string*): Hash salt. Used for both ``simple`` and ``pbkdf2``
+- **password_scheme** (*string*): Password hashing scheme. May be ``simple``,
+  ``pbkdf2`` or ``pbkdf2_sha256``.
+- **salt** (*string*): Hash salt. Used for ``simple``, ``pbkdf2`` and 
``pbkdf2_sha256``
   ``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.
+  and ``pbkdf2_sha256`` ``password_scheme`` See the
+  :ref:`configuration API <config/chttpd_auth>`:: for details.
 - **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..42b895618 100644
--- a/test/elixir/test/config_test.exs
+++ b/test/elixir/test/config_test.exs
@@ -92,7 +92,8 @@ 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_sha256-/, hash_pass) or
+             Regex.match?(~r/^-pbkdf2-/, hash_pass) or
              Regex.match?(~r/^-hashed-/, hash_pass)
 
     delete_config(context, "admins", "administrator")

Reply via email to