You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by rn...@apache.org on 2023/10/24 11:03:56 UTC
[couchdb] 02/09: Introduce pbkdf2_prf parameter
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 <rn...@apache.org>
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")