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/23 13:47:25 UTC
[couchdb] 06/06: WIP upgrade hashes on successful authentication
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 fd6bad99ee474ca0d2d83cb8464fb587bbf843bd
Author: Robert Newson <rn...@apache.org>
AuthorDate: Thu Oct 19 23:34:29 2023 +0100
WIP 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 | 81 +++++++++++++++++++++++++++++++--
src/couch/src/couch_users_db.erl | 28 +++++++++---
5 files changed, 112 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..50a313a00 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,39 @@ 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")),
+ if
+ CurrentScheme == TargetScheme andalso TargetScheme == <<"simple">> ->
+ false;
+ CurrentScheme == TargetScheme andalso TargetScheme == <<"pbkdf2">> andalso
+ CurrentPRF == TargetPRF ->
+ 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})