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