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/22 16:31:00 UTC

[couchdb] 02/02: 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 3e0c458a6de369d1ca309d19277aab0373e0856d
Author: Robert Newson <rn...@apache.org>
AuthorDate: Thu Oct 19 23:34:29 2023 +0100

    WIP upgrade hashes on successful authentication
---
 src/couch/src/couch_httpd_auth.erl      | 11 ++++++++++-
 src/couch/src/couch_password_hasher.erl | 26 +++++++++++++++++++++++++-
 src/couch/src/couch_users_db.erl        | 18 ++++++++++++++++--
 3 files changed, 51 insertions(+), 4 deletions(-)

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..ca9c9b7fc 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_admin_passwords/1]).
+-export([maybe_upgrade_password_hash/4, hash_admin_passwords/1]).
 
 -record(state, {}).
 
@@ -32,6 +32,22 @@
 %%% Public functions
 %%%===================================================================
 
+maybe_upgrade_password_hash(AuthModule, UserName, Password, 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">> ->
+            ok;
+        CurrentScheme == TargetScheme andalso TargetScheme == <<"pbkdf2">> andalso
+            CurrentPRF == TargetPRF ->
+            ok;
+        true ->
+            couch_log:notice("upgrading stored password hash for '~s'", [UserName]),
+            gen_server:cast(?MODULE, {upgrade_password_hash, AuthModule, Password, UserProps})
+    end.
+
 -spec hash_admin_passwords(Persist :: boolean()) -> Reply :: term().
 hash_admin_passwords(Persist) ->
     gen_server:cast(?MODULE, {hash_admin_passwords, Persist}).
@@ -50,6 +66,9 @@ init(_Args) ->
 handle_call(Msg, _From, #state{} = State) ->
     {stop, {invalid_call, Msg}, {invalid_call, Msg}, State}.
 
+handle_cast({upgrade_password_hash, AuthModule, Password, UserProps}, State) ->
+    upgrade_password_hash(AuthModule, Password, UserProps),
+    {noreply, State};
 handle_cast({hash_admin_passwords, Persist}, State) ->
     hash_admin_passwords_int(Persist),
     {noreply, State};
@@ -71,3 +90,8 @@ hash_admin_passwords_int(Persist) ->
         end,
         couch_passwords:get_unhashed_admins()
     ).
+
+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 3dc3dffde..2e17fb3c0 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,7 +89,6 @@ 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),
@@ -86,7 +101,6 @@ save_doc(#doc{body = {Body}} = Doc) ->
             Iterations = chttpd_util:get_chttpd_auth_config_integer(
                 "iterations", 50000
             ),
-            Salt = couch_uuids:random(),
             DerivedKey = couch_passwords:pbkdf2(
                 list_to_existing_atom(PRF), ClearPassword, Salt, Iterations
             ),