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:54 UTC
[couchdb] branch decouple_offline_hash_strength_from_online updated (1e70d90f4 -> 9c7737ecd)
This is an automated email from the ASF dual-hosted git repository.
rnewson pushed a change to branch decouple_offline_hash_strength_from_online
in repository https://gitbox.apache.org/repos/asf/couchdb.git
omit 1e70d90f4 improve assertion failure text
omit 8f5841934 remove all password derivation fields before adding back the new ones
omit 7ba8340cc upgrade hashes on successful authentication
omit a43ed5f02 enhance dev/run admin hash to pbkdf2:sha256
omit 5d60c83a0 give couch_password_hasher:hash a clearer name
omit 4d58fd94b use couch password cache
omit 975d71501 in-memory password hash cache
omit 8618ec228 Introduce pbkdf2_prf parameter
omit d51b244d9 switch PBKDF2 to fast_pbkdf2
new 2f443ea26 switch PBKDF2 to fast_pbkdf2
new d2dc2f520 Introduce pbkdf2_prf parameter
new e19904763 in-memory password hash cache
new eda7dd0fb use couch password cache
new 90e740936 give couch_password_hasher:hash a clearer name
new 228c3931c enhance dev/run admin hash to pbkdf2:sha256
new c4051891d upgrade hashes on successful authentication
new 1f33d6a0b remove all password derivation fields before adding back the new ones
new 9c7737ecd improve assertion failure text
This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version. This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:
* -- * -- B -- O -- O -- O (1e70d90f4)
\
N -- N -- N refs/heads/decouple_offline_hash_strength_from_online (9c7737ecd)
You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.
Any revisions marked "omit" are not gone; other references still
refer to them. Any revisions marked "discard" are gone forever.
The 9 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
src/couch/src/couch_passwords.erl | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
[couchdb] 01/09: switch PBKDF2 to fast_pbkdf2
Posted by rn...@apache.org.
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 2f443ea262cfe413c32a812dc911355d174d78f9
Author: Robert Newson <rn...@apache.org>
AuthorDate: Wed Oct 18 16:58:32 2023 +0100
switch PBKDF2 to fast_pbkdf2
---
rebar.config.script | 3 +-
rel/reltool.config | 6 ++--
src/couch/src/couch.app.src | 1 +
src/couch/src/couch_passwords.erl | 72 +++++----------------------------------
4 files changed, 16 insertions(+), 66 deletions(-)
diff --git a/rebar.config.script b/rebar.config.script
index 60bad79b2..1707bfad4 100644
--- a/rebar.config.script
+++ b/rebar.config.script
@@ -157,7 +157,8 @@ DepDescs = [
{jiffy, "jiffy", {tag, "CouchDB-1.0.9-2"}},
{mochiweb, "mochiweb", {tag, "v3.2.0"}},
{meck, "meck", {tag, "0.9.2"}},
-{recon, "recon", {tag, "2.5.3"}}
+{recon, "recon", {tag, "2.5.3"}},
+{fast_pbkdf2, {url, "https://github.com/esl/fast_pbkdf2"}, {tag, "1.0.2"}}
].
WithProper = lists:keyfind(with_proper, 1, CouchConfig) == {with_proper, true}.
diff --git a/rel/reltool.config b/rel/reltool.config
index 1dca6ff85..392d5c62c 100644
--- a/rel/reltool.config
+++ b/rel/reltool.config
@@ -66,7 +66,8 @@
%% extra
nouveau,
- recon
+ recon,
+ fast_pbkdf2
]},
{rel, "start_clean", "", [kernel, stdlib]},
{boot_rel, "couchdb"},
@@ -129,7 +130,8 @@
%% extra
{app, nouveau, [{incl_cond, include}]},
- {app, recon, [{incl_cond, include}]}
+ {app, recon, [{incl_cond, include}]},
+ {app, fast_pbkdf2, [{incl_cond, include}]}
]}.
{overlay_vars, "couchdb.config"}.
diff --git a/src/couch/src/couch.app.src b/src/couch/src/couch.app.src
index 6fc293ac1..2cd14ea14 100644
--- a/src/couch/src/couch.app.src
+++ b/src/couch/src/couch.app.src
@@ -38,6 +38,7 @@
% Upstream deps
ibrowse,
mochiweb,
+ fast_pbkdf2,
% ASF deps
couch_epi,
diff --git a/src/couch/src/couch_passwords.erl b/src/couch/src/couch_passwords.erl
index b89104603..ac8990861 100644
--- a/src/couch/src/couch_passwords.erl
+++ b/src/couch/src/couch_passwords.erl
@@ -113,62 +113,16 @@ pbkdf2(Password, Salt, Iterations, DerivedLength) when
Iterations > 0,
is_integer(DerivedLength)
->
- L = ceiling(DerivedLength / ?SHA1_OUTPUT_LENGTH),
- <<Bin:DerivedLength/binary, _/binary>> =
- iolist_to_binary(pbkdf2(Password, Salt, Iterations, L, 1, [])),
- {ok, couch_util:to_hex_bin(Bin)}.
-
--spec pbkdf2(binary(), binary(), integer(), integer(), integer(), iolist()) ->
- iolist().
-pbkdf2(_Password, _Salt, _Iterations, BlockCount, BlockIndex, Acc) when
- BlockIndex > BlockCount
-->
- lists:reverse(Acc);
-pbkdf2(Password, Salt, Iterations, BlockCount, BlockIndex, Acc) ->
- Block = pbkdf2(Password, Salt, Iterations, BlockIndex, 1, <<>>, <<>>),
- pbkdf2(Password, Salt, Iterations, BlockCount, BlockIndex + 1, [Block | Acc]).
-
--spec pbkdf2(
- binary(),
- binary(),
- integer(),
- integer(),
- integer(),
- binary(),
- binary()
-) -> binary().
-pbkdf2(_Password, _Salt, Iterations, _BlockIndex, Iteration, _Prev, Acc) when
- Iteration > Iterations
-->
- Acc;
-pbkdf2(Password, Salt, Iterations, BlockIndex, 1, _Prev, _Acc) ->
- InitialBlock = couch_util:hmac(
- sha,
- Password,
- <<Salt/binary, BlockIndex:32/integer>>
- ),
- pbkdf2(
- Password,
- Salt,
- Iterations,
- BlockIndex,
- 2,
- InitialBlock,
- InitialBlock
- );
-pbkdf2(Password, Salt, Iterations, BlockIndex, Iteration, Prev, Acc) ->
- Next = couch_util:hmac(sha, Password, Prev),
- pbkdf2(
- Password,
- Salt,
- Iterations,
- BlockIndex,
- Iteration + 1,
- Next,
- crypto:exor(Next, Acc)
- ).
+ 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) ->
+ crypto:hash_equals(BinA, BinB);
+verify(BinA, BinB) when is_binary(BinA), is_binary(BinB) ->
+ false.
+-else.
-spec verify(string(), string(), integer()) -> boolean().
verify([X | RestX], [Y | RestY], Result) ->
verify(RestX, RestY, (X bxor Y) bor Result);
@@ -189,12 +143,4 @@ verify(X, Y) when is_list(X) and is_list(Y) ->
end;
verify(_X, _Y) ->
false.
-
--spec ceiling(number()) -> integer().
-ceiling(X) ->
- T = erlang:trunc(X),
- case (X - T) of
- Neg when Neg < 0 -> T;
- Pos when Pos > 0 -> T + 1;
- _ -> T
- end.
+-endif.
[couchdb] 07/09: upgrade hashes on successful authentication
Posted by rn...@apache.org.
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 c4051891d976f4a1c3b1dca729d0d533b734299e
Author: Robert Newson <rn...@apache.org>
AuthorDate: Thu Oct 19 23:34:29 2023 +0100
upgrade hashes on successful authentication
---
src/chttpd/src/chttpd_auth_cache.erl | 2 +-
src/couch/src/couch_auth_cache.erl | 2 +-
src/couch/src/couch_httpd_auth.erl | 11 ++++-
src/couch/src/couch_password_hasher.erl | 85 +++++++++++++++++++++++++++++++--
src/couch/src/couch_users_db.erl | 28 ++++++++---
5 files changed, 116 insertions(+), 12 deletions(-)
diff --git a/src/chttpd/src/chttpd_auth_cache.erl b/src/chttpd/src/chttpd_auth_cache.erl
index 36157ed38..c664cd3a9 100644
--- a/src/chttpd/src/chttpd_auth_cache.erl
+++ b/src/chttpd/src/chttpd_auth_cache.erl
@@ -61,7 +61,7 @@ get_user_creds(_Req, UserName) when is_binary(UserName) ->
update_user_creds(_Req, UserDoc, _Ctx) ->
{_, Ref} = spawn_monitor(fun() ->
- case fabric:update_doc(dbname(), UserDoc, []) of
+ case fabric:update_doc(dbname(), UserDoc, [?ADMIN_CTX]) of
{ok, _} ->
exit(ok);
Else ->
diff --git a/src/couch/src/couch_auth_cache.erl b/src/couch/src/couch_auth_cache.erl
index efa8e4765..08675c941 100644
--- a/src/couch/src/couch_auth_cache.erl
+++ b/src/couch/src/couch_auth_cache.erl
@@ -54,7 +54,7 @@ get_user_creds(_Req, UserName) ->
update_user_creds(_Req, UserDoc, _AuthCtx) ->
ok = ensure_users_db_exists(),
couch_util:with_db(users_db(), fun(UserDb) ->
- {ok, _NewRev} = couch_db:update_doc(UserDb, UserDoc, []),
+ {ok, _NewRev} = couch_db:update_doc(UserDb, UserDoc, [?ADMIN_CTX]),
ok
end).
diff --git a/src/couch/src/couch_httpd_auth.erl b/src/couch/src/couch_httpd_auth.erl
index 1b4fe1ebb..555b1cc3e 100644
--- a/src/couch/src/couch_httpd_auth.erl
+++ b/src/couch/src/couch_httpd_auth.erl
@@ -113,10 +113,19 @@ default_authentication_handler(Req, AuthModule) ->
Password = ?l2b(Pass),
case authenticate(AuthModule, UserName, Password, UserProps) of
true ->
+ Roles = couch_util:get_value(<<"roles">>, UserProps, []),
+ case lists:member(<<"_admin">>, Roles) of
+ true ->
+ ok;
+ false ->
+ couch_password_hasher:maybe_upgrade_password_hash(
+ AuthModule, UserName, Password, UserProps
+ )
+ end,
Req0 = Req#httpd{
user_ctx = #user_ctx{
name = UserName,
- roles = couch_util:get_value(<<"roles">>, UserProps, [])
+ roles = Roles
}
},
case chttpd_util:get_chttpd_auth_config("secret") of
diff --git a/src/couch/src/couch_password_hasher.erl b/src/couch/src/couch_password_hasher.erl
index b34bc52d9..771a11a42 100644
--- a/src/couch/src/couch_password_hasher.erl
+++ b/src/couch/src/couch_password_hasher.erl
@@ -21,17 +21,37 @@
init/1,
handle_call/3,
handle_cast/2,
+ handle_info/2,
code_change/3
]).
--export([hash_admin_passwords/1]).
+-export([maybe_upgrade_password_hash/4, hash_admin_passwords/1]).
--record(state, {}).
+-export([worker_loop/1]).
+
+-define(IN_PROGRESS_ETS, couch_password_hasher_in_progress).
+
+-record(state, {
+ worker_pid,
+ worker_mon
+}).
%%%===================================================================
%%% Public functions
%%%===================================================================
+maybe_upgrade_password_hash(AuthModule, UserName, Password, UserProps) ->
+ NeedsUpgrade = needs_upgrade(UserProps),
+ InProgress = in_progress(AuthModule, UserName),
+ if
+ NeedsUpgrade andalso not InProgress ->
+ gen_server:cast(
+ ?MODULE, {upgrade_password_hash, AuthModule, UserName, Password, UserProps}
+ );
+ true ->
+ ok
+ end.
+
-spec hash_admin_passwords(Persist :: boolean()) -> Reply :: term().
hash_admin_passwords(Persist) ->
gen_server:cast(?MODULE, {hash_admin_passwords, Persist}).
@@ -45,17 +65,36 @@ start_link() ->
init(_Args) ->
hash_admin_passwords(true),
- {ok, #state{}}.
+ ?IN_PROGRESS_ETS = ets:new(?IN_PROGRESS_ETS, [named_table, {read_concurrency, true}]),
+ {ok, start_worker_loop(#state{})}.
handle_call(Msg, _From, #state{} = State) ->
{stop, {invalid_call, Msg}, {invalid_call, Msg}, State}.
+handle_cast({upgrade_password_hash, AuthModule, UserName, Password, UserProps}, State) ->
+ case ets:insert_new(?IN_PROGRESS_ETS, {{AuthModule, UserName}}) of
+ true ->
+ State#state.worker_pid !
+ {upgrade_password_hash, AuthModule, UserName, Password, UserProps};
+ false ->
+ ok
+ end,
+ {noreply, State};
handle_cast({hash_admin_passwords, Persist}, State) ->
hash_admin_passwords_int(Persist),
{noreply, State};
handle_cast(Msg, State) ->
{stop, {invalid_cast, Msg}, State}.
+handle_info({done, AuthModule, UserName}, State) ->
+ ets:delete(?IN_PROGRESS_ETS, {AuthModule, UserName}),
+ {noreply, State};
+handle_info({'DOWN', MonRef, _, _, normal}, #state{worker_mon = MonRef} = State) ->
+ ets:delete_all_objects(?IN_PROGRESS_ETS),
+ {noreply, start_worker_loop(State)};
+handle_info(Msg, State) ->
+ {stop, {invalid_info, Msg}, State}.
+
code_change(_OldVsn, #state{} = State, _Extra) ->
{ok, State}.
@@ -71,3 +110,43 @@ hash_admin_passwords_int(Persist) ->
end,
couch_passwords:get_unhashed_admins()
).
+
+needs_upgrade(UserProps) ->
+ CurrentScheme = couch_util:get_value(<<"password_scheme">>, UserProps, <<"simple">>),
+ TargetScheme = ?l2b(chttpd_util:get_chttpd_auth_config("password_scheme", "pbkdf2")),
+ CurrentPRF = couch_util:get_value(<<"pbkdf2_prf">>, UserProps, <<"sha">>),
+ TargetPRF = ?l2b(chttpd_util:get_chttpd_auth_config("pbkdf2_prf", "sha256")),
+ CurrentIterations = list_to_integer(couch_util:get_value(<<"iteratins">>, UserProps, "10")),
+ TargetIterations = chttpd_util:get_chttpd_auth_config_integer(
+ "iterations", 10
+ ),
+ if
+ CurrentScheme == TargetScheme andalso TargetScheme == <<"simple">> ->
+ false;
+ CurrentScheme == TargetScheme andalso TargetScheme == <<"pbkdf2">> andalso
+ CurrentPRF == TargetPRF andalso CurrentIterations == TargetIterations ->
+ false;
+ true ->
+ true
+ end.
+
+in_progress(AuthModule, UserName) ->
+ ets:member(?IN_PROGRESS_ETS, {AuthModule, UserName}).
+
+start_worker_loop(State) ->
+ {WorkerPid, WorkerMon} = spawn_monitor(?MODULE, worker_loop, [self()]),
+ State#state{worker_pid = WorkerPid, worker_mon = WorkerMon}.
+
+worker_loop(Parent) ->
+ receive
+ {upgrade_password_hash, AuthModule, UserName, Password, UserProps} ->
+ couch_log:notice("upgrading stored password hash for '~s'", [UserName]),
+ upgrade_password_hash(AuthModule, Password, UserProps),
+ erlang:send_after(5000, Parent, {done, AuthModule, UserName})
+ end,
+ worker_loop(Parent).
+
+upgrade_password_hash(AuthModule, Password, UserProps0) ->
+ UserProps1 = [{<<"password">>, Password}, {<<"preserve_salt">>, true} | UserProps0],
+ NewUserDoc = couch_doc:from_json_obj({UserProps1}),
+ AuthModule:update_user_creds(nil, NewUserDoc, ?ADMIN_CTX).
diff --git a/src/couch/src/couch_users_db.erl b/src/couch/src/couch_users_db.erl
index f8d56882a..a32591d99 100644
--- a/src/couch/src/couch_users_db.erl
+++ b/src/couch/src/couch_users_db.erl
@@ -24,6 +24,7 @@
-define(PASSWORD_SHA, <<"password_sha">>).
-define(PBKDF2, <<"pbkdf2">>).
-define(PBKDF2_PRF, <<"pbkdf2_prf">>).
+-define(PRESERVE_SALT, <<"preserve_salt">>).
-define(ITERATIONS, <<"iterations">>).
-define(SALT, <<"salt">>).
-define(replace(L, K, V), lists:keystore(K, 1, L, {K, V})).
@@ -64,6 +65,21 @@ before_doc_update(Doc, Db, _UpdateType) ->
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"),
+
+ % We preserve the salt value if requested (for a hashing upgrade, typically)
+ % in order to avoid conflicts if multiple nodes try to upgrade at the same time
+ % and to avoid invalidating existing session cookies (since the password did not
+ % change).
+ PreserveSalt = couch_util:get_value(?PRESERVE_SALT, Body, false),
+ Salt =
+ if
+ PreserveSalt ->
+ % use existing salt, if present.
+ couch_util:get_value(?SALT, Body, couch_uuids:random());
+ true ->
+ couch_uuids:random()
+ end,
+
case {couch_util:get_value(?PASSWORD, Body), Scheme} of
% server admins don't have a user-db password entry
{null, _} ->
@@ -73,20 +89,19 @@ save_doc(#doc{body = {Body}} = Doc) ->
% deprecated
{ClearPassword, "simple"} ->
ok = validate_password(ClearPassword),
- Salt = couch_uuids:random(),
PasswordSha = couch_passwords:simple(ClearPassword, Salt),
Body0 = ?replace(Body, ?PASSWORD_SCHEME, ?SIMPLE),
Body1 = ?replace(Body0, ?SALT, Salt),
Body2 = ?replace(Body1, ?PASSWORD_SHA, PasswordSha),
- Body3 = proplists:delete(?PASSWORD, Body2),
- Doc#doc{body = {Body3}};
+ Body3 = proplists:delete(?PRESERVE_SALT, Body2),
+ Body4 = proplists:delete(?PASSWORD, Body3),
+ Doc#doc{body = {Body4}};
{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(
list_to_existing_atom(PRF), ClearPassword, Salt, Iterations
),
@@ -95,8 +110,9 @@ save_doc(#doc{body = {Body}} = Doc) ->
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}};
+ Body5 = proplists:delete(?PRESERVE_SALT, Body4),
+ Body6 = proplists:delete(?PASSWORD, Body5),
+ Doc#doc{body = {Body6}};
{_ClearPassword, Scheme} ->
couch_log:error("[couch_httpd_auth] password_scheme value of '~p' is invalid.", [Scheme]),
throw({forbidden, ?PASSWORD_SERVER_ERROR})
[couchdb] 06/09: enhance dev/run admin hash to pbkdf2:sha256
Posted by rn...@apache.org.
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 228c3931c6bef5ac120f94ae2aee2c9ce2253593
Author: Robert Newson <rn...@apache.org>
AuthorDate: Sun Oct 22 17:54:23 2023 +0100
enhance dev/run admin hash to pbkdf2:sha256
---
dev/run | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/dev/run b/dev/run
index 823206ae0..707c8a4d7 100755
--- a/dev/run
+++ b/dev/run
@@ -17,6 +17,7 @@ import base64
import contextlib
import functools
import glob
+import hashlib
import inspect
import json
import ntpath
@@ -560,7 +561,7 @@ def gen_password():
return base64.b64encode(os.urandom(6)).decode()
-def hashify(pwd, salt=COMMON_SALT, iterations=10, keylen=20):
+def hashify(pwd, salt=COMMON_SALT, iterations=10, keylen=32):
"""
Implements password hashing according to:
- https://issues.apache.org/jira/browse/COUCHDB-1060
@@ -571,8 +572,8 @@ def hashify(pwd, salt=COMMON_SALT, iterations=10, keylen=20):
>>> hashify(candeira)
-pbkdf2-99eb34d97cdaa581e6ba7b5386e112c265c5c670,d1d2d4d8909c82c81b6c8184429a0739,10
"""
- derived_key = pbkdf2_hex(pwd, salt, iterations, keylen)
- return "-pbkdf2-%s,%s,%s" % (derived_key, salt, iterations)
+ derived_key = pbkdf2_hex(pwd, salt, iterations, keylen, hashfunc=hashlib.sha256)
+ return "-pbkdf2:sha256-%s,%s,%s" % (derived_key, salt, iterations)
def startup(ctx):
[couchdb] 08/09: remove all password derivation fields before adding back the new ones
Posted by rn...@apache.org.
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 1f33d6a0bd849d6e0cbfd3955426bcc2bfe8bc37
Author: Robert Newson <rn...@apache.org>
AuthorDate: Mon Oct 23 18:32:22 2023 +0100
remove all password derivation fields before adding back the new ones
---
src/couch/src/couch_users_db.erl | 48 +++++++++++++++++++++++++++-------------
1 file changed, 33 insertions(+), 15 deletions(-)
diff --git a/src/couch/src/couch_users_db.erl b/src/couch/src/couch_users_db.erl
index a32591d99..e3b914969 100644
--- a/src/couch/src/couch_users_db.erl
+++ b/src/couch/src/couch_users_db.erl
@@ -27,7 +27,6 @@
-define(PRESERVE_SALT, <<"preserve_salt">>).
-define(ITERATIONS, <<"iterations">>).
-define(SALT, <<"salt">>).
--define(replace(L, K, V), lists:keystore(K, 1, L, {K, V})).
-define(REQUIREMENT_ERROR, "Password does not conform to requirements.").
-define(PASSWORD_SERVER_ERROR, "Server cannot hash passwords at this time.").
@@ -90,12 +89,14 @@ save_doc(#doc{body = {Body}} = Doc) ->
{ClearPassword, "simple"} ->
ok = validate_password(ClearPassword),
PasswordSha = couch_passwords:simple(ClearPassword, Salt),
- Body0 = ?replace(Body, ?PASSWORD_SCHEME, ?SIMPLE),
- Body1 = ?replace(Body0, ?SALT, Salt),
- Body2 = ?replace(Body1, ?PASSWORD_SHA, PasswordSha),
- Body3 = proplists:delete(?PRESERVE_SALT, Body2),
- Body4 = proplists:delete(?PASSWORD, Body3),
- Doc#doc{body = {Body4}};
+ Body0 = remove_password_fields(Body),
+ Body1 = [
+ {?PASSWORD_SCHEME, ?SIMPLE},
+ {?SALT, Salt},
+ {?PASSWORD_SHA, PasswordSha}
+ | Body0
+ ],
+ Doc#doc{body = {Body1}};
{ClearPassword, "pbkdf2"} ->
ok = validate_password(ClearPassword),
PRF = chttpd_util:get_chttpd_auth_config("pbkdf2_prf", "sha256"),
@@ -105,19 +106,36 @@ save_doc(#doc{body = {Body}} = Doc) ->
DerivedKey = couch_passwords:pbkdf2(
list_to_existing_atom(PRF), ClearPassword, Salt, Iterations
),
- Body0 = ?replace(Body, ?PASSWORD_SCHEME, ?PBKDF2),
- 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(?PRESERVE_SALT, Body4),
- Body6 = proplists:delete(?PASSWORD, Body5),
- Doc#doc{body = {Body6}};
+ Body0 = remove_password_fields(Body),
+ Body1 = [
+ {?PASSWORD_SCHEME, ?PBKDF2},
+ {?PBKDF2_PRF, ?l2b(PRF)},
+ {?SALT, Salt},
+ {?ITERATIONS, Iterations},
+ {?DERIVED_KEY, DerivedKey}
+ | Body0
+ ],
+ Doc#doc{body = {Body1}};
{_ClearPassword, Scheme} ->
couch_log:error("[couch_httpd_auth] password_scheme value of '~p' is invalid.", [Scheme]),
throw({forbidden, ?PASSWORD_SERVER_ERROR})
end.
+remove_password_fields(Props) ->
+ lists:filter(fun not_password_field/1, Props).
+
+not_password_field({Key, _Value}) ->
+ not lists:member(Key, [
+ ?DERIVED_KEY,
+ ?ITERATIONS,
+ ?PASSWORD,
+ ?PASSWORD_SCHEME,
+ ?PASSWORD_SHA,
+ ?PRESERVE_SALT,
+ ?PBKDF2_PRF,
+ ?SALT
+ ]).
+
% Validate if a new password matches all RegExp in the password_regexp setting.
% Throws if not.
% In this function the [couch_httpd_auth] password_regexp config is parsed.
[couchdb] 02/09: Introduce pbkdf2_prf parameter
Posted by rn...@apache.org.
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")
[couchdb] 09/09: improve assertion failure text
Posted by rn...@apache.org.
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 9c7737ecdfd642c2af7eb0f68348353a1b9f4592
Author: Robert Newson <rn...@apache.org>
AuthorDate: Tue Oct 24 09:15:44 2023 +0100
improve assertion failure text
---
src/chttpd/test/eunit/chttpd_auth_hash_algorithms_tests.erl | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/chttpd/test/eunit/chttpd_auth_hash_algorithms_tests.erl b/src/chttpd/test/eunit/chttpd_auth_hash_algorithms_tests.erl
index 52f4f9e8b..3085079ad 100644
--- a/src/chttpd/test/eunit/chttpd_auth_hash_algorithms_tests.erl
+++ b/src/chttpd/test/eunit/chttpd_auth_hash_algorithms_tests.erl
@@ -88,8 +88,8 @@ test_hash_algorithm([DefaultHashAlgorithm | DecodingHashAlgorithmsList] = _, Sta
get_full_secret(?ADM_USER),
CurrentTime
),
- {ok, ReqStatus, _, _} = test_request:request(get, base_url(), [{cookie, Cookie}]),
- ?assertEqual(Status, ReqStatus),
+ {ok, ReqStatus, Hdrs, Body} = test_request:request(get, base_url(), [{cookie, Cookie}]),
+ ?assertMatch({Status, _, _}, {ReqStatus, Hdrs, Body}),
test_hash_algorithm(DecodingHashAlgorithmsList, Status).
test_hash_algorithms_should_work({_, {WorkingHashes, _, SupportedHashAlgorithms}} = _) ->
[couchdb] 05/09: give couch_password_hasher:hash a clearer name
Posted by rn...@apache.org.
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 90e740936cf73a0a3078bf13535d157e051777d3
Author: Robert Newson <rn...@apache.org>
AuthorDate: Thu Oct 19 14:18:44 2023 +0100
give couch_password_hasher:hash a clearer name
---
src/couch/src/couch_password_hasher.erl | 14 +++++++-------
src/couch/src/couch_server.erl | 2 +-
2 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/src/couch/src/couch_password_hasher.erl b/src/couch/src/couch_password_hasher.erl
index 000497694..b34bc52d9 100644
--- a/src/couch/src/couch_password_hasher.erl
+++ b/src/couch/src/couch_password_hasher.erl
@@ -24,7 +24,7 @@
code_change/3
]).
--export([hash/1]).
+-export([hash_admin_passwords/1]).
-record(state, {}).
@@ -32,9 +32,9 @@
%%% Public functions
%%%===================================================================
--spec hash(Persist :: boolean()) -> Reply :: term().
-hash(Persist) ->
- gen_server:cast(?MODULE, {hash_passwords, Persist}).
+-spec hash_admin_passwords(Persist :: boolean()) -> Reply :: term().
+hash_admin_passwords(Persist) ->
+ gen_server:cast(?MODULE, {hash_admin_passwords, Persist}).
%%%===================================================================
%%% Spawning and gen_server implementation
@@ -50,8 +50,8 @@ init(_Args) ->
handle_call(Msg, _From, #state{} = State) ->
{stop, {invalid_call, Msg}, {invalid_call, Msg}, State}.
-handle_cast({hash_passwords, Persist}, State) ->
- hash_admin_passwords(Persist),
+handle_cast({hash_admin_passwords, Persist}, State) ->
+ hash_admin_passwords_int(Persist),
{noreply, State};
handle_cast(Msg, State) ->
{stop, {invalid_cast, Msg}, State}.
@@ -63,7 +63,7 @@ code_change(_OldVsn, #state{} = State, _Extra) ->
%%% Internal functions
%%%===================================================================
-hash_admin_passwords(Persist) ->
+hash_admin_passwords_int(Persist) ->
lists:foreach(
fun({User, ClearPassword}) ->
HashedPassword = couch_passwords:hash_admin_password(ClearPassword),
diff --git a/src/couch/src/couch_server.erl b/src/couch/src/couch_server.erl
index 7dbbe4af1..94f36baab 100644
--- a/src/couch/src/couch_server.erl
+++ b/src/couch/src/couch_server.erl
@@ -357,7 +357,7 @@ handle_config_change("couchdb_engines", _, _, _, N) ->
{ok, N};
handle_config_change("admins", _, _, Persist, 1 = N) ->
% async hashing on couch_server with number 1 only
- couch_password_hasher:hash(Persist),
+ couch_password_hasher:hash_admin_passwords(Persist),
{ok, N};
handle_config_change("httpd", "authentication_handlers", _, _, 1 = N) ->
couch_httpd:stop(),
[couchdb] 04/09: use couch password cache
Posted by rn...@apache.org.
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 eda7dd0fb12e9e7b193fe6d256726ef1f8725bb8
Author: Robert Newson <rn...@apache.org>
AuthorDate: Thu Oct 19 19:48:53 2023 +0100
use couch password cache
---
src/couch/src/couch_httpd_auth.erl | 22 ++++++++++++++++++----
1 file changed, 18 insertions(+), 4 deletions(-)
diff --git a/src/couch/src/couch_httpd_auth.erl b/src/couch/src/couch_httpd_auth.erl
index 6bb4774ce..1b4fe1ebb 100644
--- a/src/couch/src/couch_httpd_auth.erl
+++ b/src/couch/src/couch_httpd_auth.erl
@@ -28,7 +28,7 @@
-export([cookie_auth_header/2]).
-export([handle_session_req/1, handle_session_req/2]).
--export([authenticate/2, verify_totp/2]).
+-export([authenticate/4, verify_totp/2]).
-export([ensure_cookie_auth_secret/0, make_cookie_time/0]).
-export([maybe_value/3]).
@@ -111,7 +111,7 @@ default_authentication_handler(Req, AuthModule) ->
reject_if_totp(UserProps),
UserName = ?l2b(User),
Password = ?l2b(Pass),
- case authenticate(Password, UserProps) of
+ case authenticate(AuthModule, UserName, Password, UserProps) of
true ->
Req0 = Req#httpd{
user_ctx = #user_ctx{
@@ -536,7 +536,7 @@ handle_session_req(#httpd{method = 'POST', mochi_req = MochiReq} = Req, AuthModu
nil -> {ok, [], nil};
Result -> Result
end,
- case authenticate(Password, UserProps) of
+ case authenticate(AuthModule, UserName, Password, UserProps) of
true ->
verify_totp(UserProps, Form),
% setup the session cookie
@@ -652,7 +652,21 @@ extract_username(Form) ->
maybe_value(_Key, undefined, _Fun) -> [];
maybe_value(Key, Else, Fun) -> [{Key, Fun(Else)}].
-authenticate(Pass, UserProps) ->
+authenticate(AuthModule, UserName, Password, UserProps) ->
+ case couch_passwords_cache:authenticate(AuthModule, UserName, Password) of
+ not_found ->
+ case authenticate_int(Password, UserProps) of
+ false ->
+ false;
+ true ->
+ couch_passwords_cache:insert(AuthModule, UserName, Password),
+ true
+ end;
+ Result when is_boolean(Result) ->
+ Result
+ end.
+
+authenticate_int(Pass, UserProps) ->
UserSalt = couch_util:get_value(<<"salt">>, UserProps, <<>>),
{PasswordHash, ExpectedHash} =
case couch_util:get_value(<<"password_scheme">>, UserProps, <<"simple">>) of
[couchdb] 03/09: in-memory password hash cache
Posted by rn...@apache.org.
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 e199047633b19501007acd0959278bcf6c1c05dc
Author: Robert Newson <rn...@apache.org>
AuthorDate: Thu Oct 19 15:18:48 2023 +0100
in-memory password hash cache
---
src/couch/src/couch_passwords_cache.erl | 54 +++++++++++++++++++++++++++++++++
src/couch/src/couch_primary_sup.erl | 14 ++++++++-
2 files changed, 67 insertions(+), 1 deletion(-)
diff --git a/src/couch/src/couch_passwords_cache.erl b/src/couch/src/couch_passwords_cache.erl
new file mode 100644
index 000000000..87370f737
--- /dev/null
+++ b/src/couch/src/couch_passwords_cache.erl
@@ -0,0 +1,54 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+% http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+%
+% password hashes on disk can take a long time to verify. This is by design, to
+% guard against offline attacks. This module adds an in-memory cache for password
+% verification to speed up verification after a successful slow verification from
+% the hash stored on disk, in order not to transfer the deliberate offline attack
+% protection to database requests.
+%
+% In memory we record a PBKDF_SHA256 derivation using a low number of iterations
+% and check against this if present. Entries in couch_passwords_cache expire automatically
+% and the maximum number of cached entries is configurable.
+
+-module(couch_passwords_cache).
+
+-define(CACHE, couch_passwords_cache_lru).
+
+-export([start_link/0]).
+
+% public api
+-export([authenticate/3, insert/3]).
+
+start_link() ->
+ gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
+
+% public functions
+-spec authenticate(AuthModule :: atom(), UserName :: binary(), Password :: binary()) ->
+ not_found | boolean().
+authenticate(AuthModule, UserName, Password) ->
+ case ets_lru:lookup_d(?CACHE, {AuthModule, UserName}) of
+ not_found ->
+ not_found;
+ {ok, {Salt, Expected}} ->
+ Actual = hash(Password, Salt),
+ couch_passwords:verify(Expected, Actual)
+ end.
+
+-spec insert(AuthModule :: atom(), UserName :: binary(), Password :: binary()) -> ok.
+insert(AuthModule, UserName, Password) ->
+ Salt = couch_uuids:random(),
+ DerivedKey = hash(Password, Salt),
+ ets_lru:insert(?CACHE, {AuthModule, UserName}, {Salt, DerivedKey}).
+
+hash(Password, Salt) ->
+ fast_pbkdf2:pbkdf2(sha256, Password, Salt, _Iterations = 5, _KeyLen = 32).
diff --git a/src/couch/src/couch_primary_sup.erl b/src/couch/src/couch_primary_sup.erl
index 1eae87160..cfb460bf8 100644
--- a/src/couch/src/couch_primary_sup.erl
+++ b/src/couch/src/couch_primary_sup.erl
@@ -23,7 +23,19 @@ init([]) ->
{couch_task_status, {couch_task_status, start_link, []}, permanent, brutal_kill, worker,
[couch_task_status]},
{couch_password_hasher, {couch_password_hasher, start_link, []}, permanent, brutal_kill,
- worker, [couch_password_hasher]}
+ worker, [couch_password_hasher]},
+ {couch_passwords_cache_lru,
+ {ets_lru, start_link, [
+ couch_passwords_cache_lru,
+
+ [
+ {max_objects,
+ config:get_integer("couch_passwords_cache", "max_objects", 10_000)},
+ {max_lifetime,
+ config:get_integer("couch_passwords_cache", "max_lifetime", 360_000)}
+ ]
+ ]},
+ permanent, 5000, worker, [ets_lru]}
] ++ couch_servers(),
{ok, {{one_for_one, 10, 3600}, Children}}.