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")
