Browse Source

Merge pull request #173 from seriyps/hide-password-in-crash-report

Hide password in crash report
Sergey Prokhorov 6 years ago
parent
commit
67ed41c57a
5 changed files with 76 additions and 35 deletions
  1. 4 1
      README.md
  2. 32 9
      src/commands/epgsql_cmd_connect.erl
  3. 15 11
      src/epgsql.erl
  4. 13 7
      src/epgsqla.erl
  5. 12 7
      src/epgsqli.erl

+ 4 - 1
README.md

@@ -66,7 +66,7 @@ connect(Opts) -> {ok, Connection :: epgsql:connection()} | {error, Reason :: epg
   Opts ::
     #{host :=     inet:ip_address() | inet:hostname(),
       username := iodata(),
-      password => iodata(),
+      password => iodata() | fun( () -> iodata() ),
       database => iodata(),
       port =>     inet:port_number(),
       ssl =>      boolean() | required,
@@ -91,6 +91,9 @@ ok = epgsql:close(C).
 
 Only `host` and `username` are mandatory, but most likely you would need `database` and `password`.
 
+- `password` - DB user password. It might be provided as string / binary or as a fun that returns
+   string / binary. Internally, plain password is wrapped to anonymous fun before it is sent to connection
+   process, so, if `connect` command crashes, plain password will not appear in crash logs.
 - `{timeout, TimeoutMs}` parameter will trigger an `{error, timeout}` result when the
    socket fails to connect within `TimeoutMs` milliseconds.
 - `ssl` if set to `true`, perform an attempt to connect in ssl mode, but continue unencrypted

+ 32 - 9
src/commands/epgsql_cmd_connect.erl

@@ -4,6 +4,7 @@
 %%%
 -module(epgsql_cmd_connect).
 -behaviour(epgsql_command).
+-export([hide_password/1, opts_hide_password/1]).
 -export([init/1, execute/2, handle_message/4]).
 -export_type([response/0, connect_error/0]).
 
@@ -41,12 +42,8 @@
 -define(AUTH_SASL_CONTINUE, 11).
 -define(AUTH_SASL_FINAL, 12).
 
-init({Host, Username, Password, Opts}) ->
-    Opts1 = maps:merge(Opts,
-                       #{host => Host,
-                         username => Username,
-                         password => Password}),
-    #connect{opts = Opts1}.
+init(#{host := _, username := _} = Opts) ->
+    #connect{opts = Opts}.
 
 execute(PgSock, #connect{opts = Opts, stage = connect} = State) ->
     #{host := Host,
@@ -97,6 +94,26 @@ execute(PgSock, #connect{stage = auth, auth_send = {PacketId, Data}} = St) ->
     {ok, PgSock, St#connect{auth_send = undefined}}.
 
 
+%% @doc Replace `password' in Opts map with obfuscated one
+opts_hide_password(#{password := Password} = Opts) ->
+    HiddenPassword = hide_password(Password),
+    Opts#{password => HiddenPassword};
+opts_hide_password(Opts) -> Opts.
+
+
+%% @doc this function wraps plaintext password to a lambda function, so, if
+%% epgsql_sock process crashes when executing `connect` command, password will
+%% not appear in a crash log
+-spec hide_password(iodata()) -> fun( () -> iodata() ).
+hide_password(Password) when is_list(Password);
+                             is_binary(Password) ->
+    fun() ->
+            Password
+    end;
+hide_password(PasswordFun) when is_function(PasswordFun, 0) ->
+    PasswordFun.
+
+
 maybe_ssl(S, false, _, PgSock) ->
     epgsql_sock:set_net_socket(gen_tcp, S, PgSock);
 maybe_ssl(S, Flag, Opts, PgSock) ->
@@ -164,14 +181,14 @@ auth_handle(Data, PgSock, #connect{auth_fun = Fun, auth_state = AuthSt} = St) ->
 
 %% AuthenticationCleartextPassword
 auth_cleartext(init, _AuthState, #connect{opts = Opts}) ->
-    Password = maps:get(password, Opts),
+    Password = get_password(Opts),
     {send, ?PASSWORD, [Password, 0], undefined};
 auth_cleartext(_, _, _) -> unknown.
 
 %% AuthenticationMD5Password
 auth_md5(init, Salt, #connect{opts = Opts}) ->
     User = maps:get(username, Opts),
-    Password = maps:get(password, Opts),
+    Password = get_password(Opts),
     Digest1 = hex(erlang:md5([Password, User])),
     Str = ["md5", hex(erlang:md5([Digest1, Salt])), 0],
     {send, ?PASSWORD, Str, undefined};
@@ -186,7 +203,7 @@ auth_scram(init, undefined, #connect{opts = Opts}) ->
     {send, ?SASL_ANY_RESPONSE, SaslInitialResponse, {auth_request, Nonce}};
 auth_scram(<<?AUTH_SASL_CONTINUE:?int32, ServerFirst/binary>>, {auth_request, Nonce}, #connect{opts = Opts}) ->
     User = maps:get(username, Opts),
-    Password = maps:get(password, Opts),
+    Password = get_password(Opts),
     ServerFirstParts = epgsql_scram:parse_server_first(ServerFirst, Nonce),
     {ClientFinalMessage, ServerProof} = epgsql_scram:get_client_final(ServerFirstParts, Nonce, User, Password),
     {send, ?SASL_ANY_RESPONSE, ClientFinalMessage, {server_final, ServerProof}};
@@ -239,6 +256,12 @@ handle_message(?ERROR, Err, Sock, #connect{stage = Stage} = _State) when Stage =
 handle_message(_, _, _, _) ->
     unknown.
 
+
+get_password(Opts) ->
+    PasswordFun = maps:get(password, Opts),
+    PasswordFun().
+
+
 hex(Bin) ->
     HChar = fun(N) when N < 10 -> $0 + N;
                (N) when N < 16 -> $W + N

+ 15 - 11
src/epgsql.erl

@@ -61,7 +61,7 @@
         [connect_option()]
       | #{host => host(),
           username => string(),
-          password => string(),
+          password => iodata() | fun( () -> iodata() ),
           database => string(),
           port => inet:port_number(),
           ssl => boolean() | required,
@@ -124,12 +124,9 @@
 %% -- client interface --
 -spec connect(connect_opts())
         -> {ok, Connection :: connection()} | {error, Reason :: connect_error()}.
-connect(Settings0) ->
-    Settings = to_map(Settings0),
-    Host = maps:get(host, Settings, "localhost"),
-    Username = maps:get(username, Settings, os:getenv("USER")),
-    Password = maps:get(password, Settings, ""),
-    connect(Host, Username, Password, Settings).
+connect(Opts) ->
+    {ok, C} = epgsql_sock:start_link(),
+    call_connect(C, Opts).
 
 connect(Host, Opts) ->
     connect(Host, os:getenv("USER"), "", Opts).
@@ -152,11 +149,17 @@ connect(Host, Username, Password, Opts) ->
 
 -spec connect(connection(), host(), string(), string(), connect_opts())
         -> {ok, Connection :: connection()} | {error, Reason :: connect_error()}.
-connect(C, Host, Username, Password, Opts0) ->
-    Opts = to_map(Opts0),
-    %% TODO connect timeout
+connect(C, Host, Username, Password, Opts) ->
+    Opts1 = maps:merge(epgsql:to_map(Opts),
+                       #{host => Host,
+                         username => Username,
+                         password => Password}),
+    call_connect(C, Opts1).
+
+call_connect(C, Opts) ->
+    Opts1 = epgsql_cmd_connect:opts_hide_password(Opts),
     case epgsql_sock:sync_command(
-           C, epgsql_cmd_connect, {Host, Username, Password, Opts}) of
+           C, epgsql_cmd_connect, Opts1) of
         connected ->
             %% If following call fails for you, try to add {codecs, []} connect option
             {ok, _} = maybe_update_typecache(C, Opts),
@@ -165,6 +168,7 @@ connect(C, Host, Username, Password, Opts0) ->
             Error
     end.
 
+
 maybe_update_typecache(C, Opts) ->
     maybe_update_typecache(C, maps:get(replication, Opts, undefined), maps:get(codecs, Opts, undefined)).
 

+ 13 - 7
src/epgsqla.erl

@@ -29,11 +29,8 @@ start_link() ->
     epgsql_sock:start_link().
 
 connect(Opts) ->
-    Settings = epgsql:to_map(Opts),
-    Host = maps:get(host, Settings, "localhost"),
-    Username = maps:get(username, Settings, os:getenv("USER")),
-    Password = maps:get(password, Settings, ""),
-    connect(Host, Username, Password, Settings).
+    {ok, C} = epgsql_sock:start_link(),
+    call_connect(C, Opts).
 
 connect(Host, Opts) ->
     connect(Host, os:getenv("USER"), "", Opts).
@@ -48,9 +45,18 @@ connect(Host, Username, Password, Opts) ->
 -spec connect(epgsql:connection(), inet:ip_address() | inet:hostname(),
               string(), string(), epgsql:connect_opts()) -> reference().
 connect(C, Host, Username, Password, Opts) ->
-    Opts1 = epgsql:to_map(Opts),
+    Opts1 = maps:merge(epgsql:to_map(Opts),
+                       #{host => Host,
+                         username => Username,
+                         password => Password}),
+    call_connect(C, Opts1).
+
+-spec call_connect(epgsql:connection(), epgsql:connect_opts()) -> reference().
+call_connect(C, Opts) when is_map(Opts) ->
+    Opts1 = epgsql_cmd_connect:opts_hide_password(Opts),
     complete_connect(
-      C, cast(C, epgsql_cmd_connect, {Host, Username, Password, Opts1}), Opts1).
+      C, cast(C, epgsql_cmd_connect, Opts1), Opts1).
+
 
 -spec close(epgsql:connection()) -> ok.
 close(C) ->

+ 12 - 7
src/epgsqli.erl

@@ -28,11 +28,8 @@ start_link() ->
     epgsql_sock:start_link().
 
 connect(Opts) ->
-    Settings = epgsql:to_map(Opts),
-    Host = maps:get(host, Settings, "localhost"),
-    Username = maps:get(username, Settings, os:getenv("USER")),
-    Password = maps:get(password, Settings, ""),
-    connect(Host, Username, Password, Settings).
+    {ok, C} = epgsql_sock:start_link(),
+    call_connect(C, Opts).
 
 connect(Host, Opts) ->
     connect(Host, os:getenv("USER"), "", Opts).
@@ -47,9 +44,17 @@ connect(Host, Username, Password, Opts) ->
 -spec connect(epgsql:connection(), inet:ip_address() | inet:hostname(),
               string(), string(), epgsql:connect_opts()) -> reference().
 connect(C, Host, Username, Password, Opts) ->
-    Opts1 = epgsql:to_map(Opts),
+    Opts1 = maps:merge(epgsql:to_map(Opts),
+                       #{host => Host,
+                         username => Username,
+                         password => Password}),
+    call_connect(C, Opts1).
+
+call_connect(C, Opts) ->
+    Opts1 = epgsql_cmd_connect:opts_hide_password(Opts),
     epgsqla:complete_connect(
-      C, incremental(C, epgsql_cmd_connect, {Host, Username, Password, Opts1}), Opts1).
+      C, incremental(C, epgsql_cmd_connect, Opts1), Opts1).
+
 
 -spec close(epgsql:connection()) -> ok.
 close(C) ->