New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Change AuthenticationPlugin interface to use char[] rather than String #2420
feat: Change AuthenticationPlugin interface to use char[] rather than String #2420
Conversation
throw new PSQLException( | ||
GT.tr( | ||
"The server requested SCRAM-based authentication, but the password is an empty string."), | ||
PSQLState.CONNECTION_REJECTED); | ||
} | ||
scramAuthenticator = new org.postgresql.jre7.sasl.ScramAuthenticator(user, castNonNull(password), pgStream); | ||
scramAuthenticator = new org.postgresql.jre7.sasl.ScramAuthenticator(user, castNonNull(String.valueOf(password)), pgStream); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should probably consider changing ScramAuthenticator
to char[]
as well
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes I mention that in the PR. It's self-contained so somebody can follow up with that separately.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
seems we should also refactor it to move it out of ...jre7.. as well
Is there any reason not to change it to char[] now ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would agree with @sehrope , the key point for now is to get the API right == move to char[]
in the interface.
The implementation can be adjusted later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So if you have no other issues should we merge this then ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ultimately we need to call ongres/scram, and they don't seem to support char[]
yet: https://gitlab.com/ongresinc/scram/-/issues/14
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jorsol can you provide some insight/suppport here ?
pgjdbc/src/main/java/org/postgresql/core/AuthenticationPluginManager.java
Outdated
Show resolved
Hide resolved
@@ -37,12 +37,16 @@ private AuthenticationPluginManager() { | |||
* @return The password to use for authentication or null if none is available | |||
* @throws PSQLException Throws a PSQLException if the plugin class cannot be instantiated | |||
*/ | |||
public static @Nullable String getPassword(AuthenticationRequestType type, Properties info) throws PSQLException { | |||
public static char @Nullable [] getPassword(AuthenticationRequestType type, Properties info) throws PSQLException { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you think of adding the following to the javadoc?
The callers should erase the contents of `char[] password` with 0 chars like in
{@code Arrays.fill(password, (char) 0)} after the password is used for security reasons.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes that's a good idea. I'm going to add that to the javadoc for the interface as that's the only part that a user would interact with. And it will be the user's responsibility to clear the array as they have ownership of it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Which “user” are you talking about? The caller of this api (eg. the driver itself as it needs password to connect) or the user of the driver who is implementing class to provides password dynamically?
It seems to me we should define the api to always have to return a new ‘char[]’ and make the driver responsible for clearing once value no longer needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@vlsi @bokken Yes the more I'm thinking about this, the hairier it'd get to have the caller clear the char[]
array as it's provided to us in the plugin interface, but the end user cannot clear it until after the connection is established which might be handled somewhere else entirely. I'm going to change the code to wipe it out after use and make a note in the Javadoc for the interface that any value provided will be destroyed after use. So the caller will be responsible for providing a new char[] array each time the same method is called.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Which “user” are you talking about?
Anyone who calls char @Nullable [] getPassword(
method.
Mainly I expect that it is the driver who will call the method.
However, the plugin implementation should understand how the value would be used, so it still makes sense to clarify the intention:
- The value could be erased after it is returned in
getPassword
. For instance, if the plugin requests the password from some sort of token server, the plugin should erase all the other copies of the password (e.g. clear buffers, etc, etc) - The caller of
getPassword
(==driver in 99% of the cases) would erasechar[]
after use
That ensures no passwords keep floating around after use.
pgjdbc/src/main/java/org/postgresql/core/AuthenticationPluginManager.java
Outdated
Show resolved
Hide resolved
a0cab93
to
7da5096
Compare
I pushed an update to this PR that has the driver clear the user provided char[] array after use. Rather than have the internal code of the driver provide password as a return value, I made The one spot where the callback code is a bit odd is for the creation of the SASL authenticator as the compiler doesn't let you initialize it in an anonymous block as the variable reference isn't final. I handled that by wrapping the variable declaration in an single entry array: I think that's still better than having to sprinkle clean up code all over the place. The other option was having This also updates the comments in the AuthenticationPlugin interface to make clear that every invocation of the |
7da5096
to
71b0ce0
Compare
One more push to make the null checker happy and I think we're be good. |
71b0ce0
to
6224c69
Compare
AuthenticationPluginManager.withEncodedPassword(AuthenticationRequestType.MD5_PASSWORD, info, encodedPassword -> { | ||
byte[] digest = MD5Digest.encode(user.getBytes(StandardCharsets.UTF_8), encodedPassword, md5Salt); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should withEncodedPassword be confined to MD5Digest.encode
only?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The cleartext code uses it as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you please clarify?
You do not need encodedPassword past MD5Digest.encode, so why make a bigger withEncodedPassword block?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh I thought you were asking why that method exists and meant that it's also used when sending the plaintext password.
In all those cases I wrapped the entire sub-block that is using the password or encoded password to keep the changeset simpler.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The intention should be to minimize the use of the password, and erase it as soon as it is no longer needed.
In this case, making the block smaller yields easier to follow changeset.
However, I guess we need to erase md5-encoded block as well, as it basically allows relay attack.
pgjdbc/src/main/java/org/postgresql/core/v3/ConnectionFactoryImpl.java
Outdated
Show resolved
Hide resolved
FYI the AppVeyor failure is the usual query timeout race condition. The other AppVeyor build ran fine. |
6224c69
to
3d1ce1f
Compare
Pushed another that moves as much as possible out of the Also fills zeroes into the md5 digest buffer after it's done being used. Here's the diff relative to the previous push as I forced atop it: diff --git a/pgjdbc/src/main/java/org/postgresql/core/v3/ConnectionFactoryImpl.java b/pgjdbc/src/main/java/org/postgresql/core/v3/ConnectionFactoryImpl.java
index d42c1535..69efc796 100644
--- a/pgjdbc/src/main/java/org/postgresql/core/v3/ConnectionFactoryImpl.java
+++ b/pgjdbc/src/main/java/org/postgresql/core/v3/ConnectionFactoryImpl.java
@@ -668,12 +668,16 @@ public class ConnectionFactoryImpl extends ConnectionFactory {
LOGGER.log(Level.FINEST, " FE=> Password(md5digest={0})", new String(digest, StandardCharsets.US_ASCII));
}
- pgStream.sendChar('p');
- pgStream.sendInteger4(4 + digest.length + 1);
- pgStream.send(digest);
- pgStream.sendChar(0);
- pgStream.flush();
+ try {
+ pgStream.sendChar('p');
+ pgStream.sendInteger4(4 + digest.length + 1);
+ pgStream.send(digest);
+ } finally {
+ java.util.Arrays.fill(digest, (byte) 0);
+ }
});
+ pgStream.sendChar(0);
+ pgStream.flush();
break;
}
@@ -686,9 +690,9 @@ public class ConnectionFactoryImpl extends ConnectionFactory {
pgStream.sendChar('p');
pgStream.sendInteger4(4 + encodedPassword.length + 1);
pgStream.send(encodedPassword);
- pgStream.sendChar(0);
- pgStream.flush();
});
+ pgStream.sendChar(0);
+ pgStream.flush();
break;
}
@@ -791,9 +795,9 @@ public class ConnectionFactoryImpl extends ConnectionFactory {
PSQLState.CONNECTION_REJECTED);
}
scramAuthenticator[0] = new org.postgresql.jre7.sasl.ScramAuthenticator(user, castNonNull(String.valueOf(password)), pgStream);
- scramAuthenticator[0].processServerMechanismsAndInit();
- scramAuthenticator[0].sendScramClientFirstMessage();
});
+ castNonNull(scramAuthenticator[0]).processServerMechanismsAndInit();
+ castNonNull(scramAuthenticator[0]).sendScramClientFirstMessage();
// This works as follows:
// 1. When tests is run from IDE, it is assumed SCRAM library is on the classpath
// 2. In regular build for Java < 8 this `if` is deactivated and the code always throws |
Are we waiting for anything else here ? |
I don't think so. The only related item would be clearing the internal stream buffers but I don't want to risk messing with that here. @vlsi Any other comments? Otherwise I'm going to merge this in a bit. |
@vlsi I didn't add a generic returning method for the callbacks so that the caller does not return back a value from the callback handler that potentially contains the password or a derivative:
|
@@ -657,25 +658,27 @@ private void doAuthentication(PGStream pgStream, String host, String user, Prope | |||
switch (areq) { | |||
case AUTH_REQ_MD5: { | |||
byte[] md5Salt = pgStream.receive(4); | |||
if (LOGGER.isLoggable(Level.FINEST)) { | |||
if (false && LOGGER.isLoggable(Level.FINEST)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why the false &&
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can probably keep it as is, however, it just bothers me we log credentials in plain text
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's likely there to allow debugging the protocol so replacing it with false &&
wouldn't make sense. Either we'd remove it entirely or put low level security related logging behind some other feature flag.
Either way it does not belong in this PR and should be done separately.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree it does not belong to the PR, however, the PR does touch the related lines, so I think it would be fine to disable the log for now, and later we can do something with it.
|
My comments regarding avoid returning the value or its derivatives were for future developers of the driver. Not just us. I appreciate the feedback and review, but you shouldn't push changes atop a PR that go directly against something that was explained without at least addressing the topic. It wasn't an arbitrary decision and I outlined why it was implemented like that. Anyway, I'm not particularly wed to not allowing the return values. I'm going to back out the logging changes (that |
Unfortunately, there's no way "missing return value" ensures the future developers would be safe. For instance, the change when you converted That is why I think returning values is just fine, and enforcing I agree the whole thing sounds like it touches more code than it was intended in the first place as in "only change the return type in the AuthenticationPlugin API". However, I believe, it is important to ensure the returned value from the authentication plugin is usable, so making sure the core works with If we go back to the signature: char @Nullable [] getPassword(AuthenticationRequestType type) throws PSQLException; then I would say it is a bit strange that we pass half of the parameters via The thing is we might have multiple hosts in the connection url, and we might be in failover mode. Then, the auth plugin might be interested to know where we are connecting as different servers (e.g. primary and the replica) might have different credentials. |
… String Changes AuthenticationPlugin interface for dynamic passwords to supply passwords as a char[] rather than a String. This changes the currently unreleased public interface of AuthenticationPlugin and allows the driver to clear the user provided char[] array after it is finished using it for authentication. Users implementing that interface must ensure that each invocation of the method provides a new char[] array as the contents will be filled with zeroes by the driver after use. Call sites within the driver have been updated to use the char[] directly wherever possible. This includes direct usage in the GSS authentication code paths that internally were already converting the String password into a char[] for internal usage. The SASL (i.e. "SCRAM") internals have not been updated to use a char[] array as the entirety of that library uses String types for provided passwords. Assuming that it is not exposed in other parts of the driver, that could be updated as a standalone PR. For now the entrypoint from the ConnectionFactoryImpl into the SASL library simply converts the char[] array to a String at it's single usage point. Co-Authored-By: Vladimir Sitnikov <sitnikov.vladimir@gmail.com>
23c8266
to
261864a
Compare
The Those other args might be helpful but if we can add it to the end user interface with a default implementation that points to the single arg version. Let's have at least one person come back with a specific use case first in case there is something else we'd need to add as well. I pushed an update and added you as a co-author. It has the last round of your changes minus the I think this good to merge. |
Having 0 users is a good sign that the feature should not be merged. Just a reminder, log4shell vulnerability appeared in log4j 2.x sources "just in case", "somebody might need variable replacement and jndi resolution". The sad thing is that nobody really asked for that replacement feature. |
This is a bit of a catch-22 how would we have users without merging the feature in ? |
By "no users" I was referring to no users asking for additional arguments be included in the password callback to justify expanding it further. Outside of host / port, the TLS parameters immediately come to mind as potentially useful. Rather than guess though, since nobody is explicitly asking for any of that as of yet, I'm leaving it out. In contrast, we have multiple users asking for the ability to specify a dynamic password at connection time, such in this original issue going back to last March: #2102 |
Updates the recently introduced, but not yet released, AuthenticationPlugin interface to use a
char[]
array rather than a String.This was simpler than I thought for the majority of the call sites as both PLAIN and MD5 used an byte[] encoded password and GSS already used a char[] array. All that remained was SCRAM which still used String for all it's internals so I made the ConnectionFactoryImpl create one at the only call site that interacts with the password. A follow up PR could clean up the internals of the SASL library to use char[] rather than String.
Two specific points to review:
@Nullable
make sense? It's weird having it in between the type and array declaration but from the rest of the codebase I think that's how it's supposed to work.char[]
password array in the GSS handler? I don't think so as any usage of it should complete beforeMakeGSS.authenticate(...)
returns. We could push the password retrieval even deeper into that function so it happens at the last moment possible, I didn't go down that road as I'm trying to minimize the number of things we're actually changing.Thoughts? @vlsi @davecramer