Skip to content
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

Merged
merged 1 commit into from Jan 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view

This file was deleted.

@@ -0,0 +1,124 @@
/*
* Copyright (c) 2021, PostgreSQL Global Development Group
* See the LICENSE file in the project root for more information.
*/

package org.postgresql.core.v3;

import org.postgresql.PGProperty;
import org.postgresql.plugin.AuthenticationPlugin;
import org.postgresql.plugin.AuthenticationRequestType;
import org.postgresql.util.GT;
import org.postgresql.util.ObjectFactory;
import org.postgresql.util.PSQLException;
import org.postgresql.util.PSQLState;

import org.checkerframework.checker.nullness.qual.Nullable;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;

class AuthenticationPluginManager {
private static final Logger LOGGER = Logger.getLogger(AuthenticationPluginManager.class.getName());

@FunctionalInterface
public interface PasswordAction<T, R> {
R apply(T password) throws PSQLException, IOException;
}

private AuthenticationPluginManager() {
}

/**
* If a password is requested by the server during connection initiation, this
* method will be invoked to supply the password. This method will only be
* invoked if the server actually requests a password, e.g. trust authentication
* will skip it entirely.
*
* <p>The caller provides a action method that will be invoked with the {@code char[]}
* password. After completion, for security reasons the {@code char[]} array will be
* wiped by filling it with zeroes. Callers must not rely on being able to read
* the password {@code char[]} after the action has completed.</p>
*
* @param type The authentication type that is being requested
* @param info The connection properties for the connection
* @param action The action to invoke with the password
* @throws PSQLException Throws a PSQLException if the plugin class cannot be instantiated
* @throws IOException Bubbles up any thrown IOException from the provided action
*/
public static <T> T withPassword(AuthenticationRequestType type, Properties info,
PasswordAction<char @Nullable [], T> action) throws PSQLException, IOException {
char[] password = null;

String authPluginClassName = PGProperty.AUTHENTICATION_PLUGIN_CLASS_NAME.get(info);

if (authPluginClassName == null || authPluginClassName.equals("")) {
// Default auth plugin simply pulls password directly from connection properties
String passwordText = PGProperty.PASSWORD.get(info);
if (passwordText != null) {
password = passwordText.toCharArray();
}
} else {
AuthenticationPlugin authPlugin;
try {
authPlugin = (AuthenticationPlugin) ObjectFactory.instantiate(authPluginClassName, info,
false, null);
} catch (Exception ex) {
LOGGER.log(Level.FINE, "Unable to load Authentication Plugin " + ex.toString());
throw new PSQLException(ex.getMessage(), PSQLState.UNEXPECTED_ERROR);
}

password = authPlugin.getPassword(type);
}

try {
return action.apply(password);
} finally {
if (password != null) {
java.util.Arrays.fill(password, (char) 0);
}
}
}

/**
* Helper that wraps {@link #withPassword(AuthenticationRequestType, Properties, PasswordAction)}, checks that it is not-null, and encodes
* it as a byte array. Used by internal code paths that require an encoded password
* that may be an empty string, but not null.
*
* <p>The caller provides a callback method that will be invoked with the {@code byte[]}
* encoded password. After completion, for security reasons the {@code byte[]} array will be
* wiped by filling it with zeroes. Callers must not rely on being able to read
* the password {@code byte[]} after the callback has completed.</p>

* @param type The authentication type that is being requested
* @param info The connection properties for the connection
* @param action The action to invoke with the encoded password
* @throws PSQLException Throws a PSQLException if the plugin class cannot be instantiated or if the retrieved password is null.
* @throws IOException Bubbles up any thrown IOException from the provided callback
*/
public static <T> T withEncodedPassword(AuthenticationRequestType type, Properties info,
PasswordAction<byte[], T> action) throws PSQLException, IOException {
byte[] encodedPassword = withPassword(type, info, password -> {
if (password == null) {
throw new PSQLException(
GT.tr("The server requested password-based authentication, but no password was provided."),
PSQLState.CONNECTION_REJECTED);
}
ByteBuffer buf = StandardCharsets.UTF_8.encode(CharBuffer.wrap(password));
byte[] bytes = new byte[buf.limit()];
buf.get(bytes);
return bytes;
});

try {
return action.apply(encodedPassword);
} finally {
java.util.Arrays.fill(encodedPassword, (byte) 0);
}
}
}
Expand Up @@ -9,7 +9,6 @@
import static org.postgresql.util.internal.Nullness.castNonNull;

import org.postgresql.PGProperty;
import org.postgresql.core.AuthenticationPluginManager;
import org.postgresql.core.ConnectionFactory;
import org.postgresql.core.PGStream;
import org.postgresql.core.QueryExecutor;
Expand Down Expand Up @@ -497,12 +496,14 @@ private PGStream enableGSSEncrypted(PGStream pgStream, GSSEncMode gssEncMode, St
case 'G':
LOGGER.log(Level.FINEST, " <=BE GSSEncryptedOk");
try {
String password = AuthenticationPluginManager.getPassword(AuthenticationRequestType.GSS, info);
org.postgresql.gss.MakeGSS.authenticate(true, pgStream, host, user, password,
PGProperty.JAAS_APPLICATION_NAME.get(info),
PGProperty.KERBEROS_SERVER_NAME.get(info), false, // TODO: fix this
PGProperty.JAAS_LOGIN.getBoolean(info),
PGProperty.LOG_SERVER_ERROR_DETAIL.getBoolean(info));
AuthenticationPluginManager.withPassword(AuthenticationRequestType.GSS, info, password -> {
org.postgresql.gss.MakeGSS.authenticate(true, pgStream, host, user, password,
PGProperty.JAAS_APPLICATION_NAME.get(info),
PGProperty.KERBEROS_SERVER_NAME.get(info), false, // TODO: fix this
PGProperty.JAAS_LOGIN.getBoolean(info),
PGProperty.LOG_SERVER_ERROR_DETAIL.getBoolean(info));
return void.class;
});
return pgStream;
} catch (PSQLException ex) {
// allow the connection to proceed
Expand Down Expand Up @@ -660,17 +661,23 @@ private void doAuthentication(PGStream pgStream, String host, String user, Prope
LOGGER.log(Level.FINEST, " <=BE AuthenticationReqMD5(salt={0})", Utils.toHexString(md5Salt));
}

byte[] encodedPassword = AuthenticationPluginManager.getEncodedPassword(AuthenticationRequestType.MD5_PASSWORD, info);
byte[] digest =
MD5Digest.encode(user.getBytes(StandardCharsets.UTF_8), encodedPassword, md5Salt);
byte[] digest = AuthenticationPluginManager.withEncodedPassword(
AuthenticationRequestType.MD5_PASSWORD, info,
encodedPassword -> MD5Digest.encode(user.getBytes(StandardCharsets.UTF_8),
encodedPassword, md5Salt)
);

if (LOGGER.isLoggable(Level.FINEST)) {
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);
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();

Expand All @@ -681,11 +688,12 @@ private void doAuthentication(PGStream pgStream, String host, String user, Prope
LOGGER.log(Level.FINEST, "<=BE AuthenticationReqPassword");
LOGGER.log(Level.FINEST, " FE=> Password(password=<not shown>)");

byte[] encodedPassword = AuthenticationPluginManager.getEncodedPassword(AuthenticationRequestType.CLEARTEXT_PASSWORD, info);

pgStream.sendChar('p');
pgStream.sendInteger4(4 + encodedPassword.length + 1);
pgStream.send(encodedPassword);
AuthenticationPluginManager.withEncodedPassword(AuthenticationRequestType.CLEARTEXT_PASSWORD, info, encodedPassword -> {
pgStream.sendChar('p');
pgStream.sendInteger4(4 + encodedPassword.length + 1);
pgStream.send(encodedPassword);
return void.class;
});
pgStream.sendChar(0);
pgStream.flush();

Expand Down Expand Up @@ -756,12 +764,14 @@ private void doAuthentication(PGStream pgStream, String host, String user, Prope
castNonNull(sspiClient).startSSPI();
} else {
/* Use JGSS's GSSAPI for this request */
String password = AuthenticationPluginManager.getPassword(AuthenticationRequestType.GSS, info);
org.postgresql.gss.MakeGSS.authenticate(false, pgStream, host, user, password,
PGProperty.JAAS_APPLICATION_NAME.get(info),
PGProperty.KERBEROS_SERVER_NAME.get(info), usespnego,
PGProperty.JAAS_LOGIN.getBoolean(info),
PGProperty.LOG_SERVER_ERROR_DETAIL.getBoolean(info));
AuthenticationPluginManager.withPassword(AuthenticationRequestType.GSS, info, password -> {
org.postgresql.gss.MakeGSS.authenticate(false, pgStream, host, user, password,
PGProperty.JAAS_APPLICATION_NAME.get(info),
PGProperty.KERBEROS_SERVER_NAME.get(info), usespnego,
PGProperty.JAAS_LOGIN.getBoolean(info),
PGProperty.LOG_SERVER_ERROR_DETAIL.getBoolean(info));
return void.class;
});
}
break;

Expand All @@ -775,20 +785,21 @@ private void doAuthentication(PGStream pgStream, String host, String user, Prope
case AUTH_REQ_SASL:
LOGGER.log(Level.FINEST, " <=BE AuthenticationSASL");

String password = AuthenticationPluginManager.getPassword(AuthenticationRequestType.SASL, info);
if (password == null) {
throw new PSQLException(
GT.tr(
"The server requested SCRAM-based authentication, but no password was provided."),
PSQLState.CONNECTION_REJECTED);
}
if (password.equals("")) {
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 = AuthenticationPluginManager.withPassword(AuthenticationRequestType.SASL, info, password -> {
if (password == null) {
throw new PSQLException(
GT.tr(
"The server requested SCRAM-based authentication, but no password was provided."),
PSQLState.CONNECTION_REJECTED);
}
if (password.length == 0) {
throw new PSQLException(
GT.tr(
"The server requested SCRAM-based authentication, but the password is an empty string."),
PSQLState.CONNECTION_REJECTED);
}
return new org.postgresql.jre7.sasl.ScramAuthenticator(user, String.valueOf(password), pgStream);
});
scramAuthenticator.processServerMechanismsAndInit();
scramAuthenticator.sendScramClientFirstMessage();
// This works as follows:
Expand Down
Expand Up @@ -23,9 +23,9 @@
class GSSCallbackHandler implements CallbackHandler {

private final String user;
private final @Nullable String password;
private final char @Nullable [] password;

GSSCallbackHandler(String user, @Nullable String password) {
GSSCallbackHandler(String user, char @Nullable [] password) {
this.user = user;
this.password = password;
}
Expand Down Expand Up @@ -56,7 +56,7 @@ public void handle(Callback[] callbacks) throws IOException, UnsupportedCallback
if (password == null) {
throw new IOException("No cached kerberos ticket found and no password supplied.");
}
pc.setPassword(password.toCharArray());
pc.setPassword(password);
} else {
throw new UnsupportedCallbackException(callback, "Unrecognized Callback");
}
Expand Down
2 changes: 1 addition & 1 deletion pgjdbc/src/main/java/org/postgresql/gss/MakeGSS.java
Expand Up @@ -27,7 +27,7 @@ public class MakeGSS {
private static final Logger LOGGER = Logger.getLogger(MakeGSS.class.getName());

public static void authenticate(boolean encrypted,
PGStream pgStream, String host, String user, @Nullable String password,
PGStream pgStream, String host, String user, char @Nullable [] password,
@Nullable String jaasApplicationName, @Nullable String kerberosServerName,
boolean useSpnego, boolean jaasLogin,
boolean logServerErrorDetail)
Expand Down
Expand Up @@ -10,7 +10,24 @@
import org.checkerframework.checker.nullness.qual.Nullable;

public interface AuthenticationPlugin {
@Nullable
String getPassword(AuthenticationRequestType type) throws PSQLException;

/**
* Callback method to provide the password to use for authentication.
*
* <p>Implementers can also check the authentication type to ensure that the
* authentication handshake is using a specific authentication method (e.g. SASL)
* or avoiding a specific one (e.g. cleartext).</p>
*
* <p>For security reasons, the driver will wipe the contents of the array returned
* by this method after it has been used for authentication.</p>
*
* <p><b>Implementers must provide a new array each time this method is invoked as
* the previous contents will have been wiped.</b></p>
*
* @param type The authentication method that the server is requesting
* @return The password to use or null if no password is available
* @throws PSQLException if something goes wrong supplying the password
*/
char @Nullable [] getPassword(AuthenticationRequestType type) throws PSQLException;
vlsi marked this conversation as resolved.
Show resolved Hide resolved

}