postgresql-jdbc.git / summary / log / commit / refs

commit    befea18d153dda7814daef4e036d3f5daf8de1e5
Author:   Álvaro Hernández Tortosa <aht@8kdata.com>
Commit:   Dave Cramer <davecramer@gmail.com>
Date:     Tue Nov 21 11:42:28 2017 +0000

    Add SCRAM-SHA-256 support (#842)
    
    * Add SCRAM-SHA-256 support
    
    PostgreSQL 10 comes with SCRAM-SHA-256 support. This commit introduces
    support for it.
    
    Work is based on an external dependency, the SCRAM client library:
    
    	https://github.com/ongres/scram/
    
    which is now imported as a dependency.
    
    TODO:
    	- SCRAM client library will be improved, on its own.
    	  Particularly, StringPrep support needs to be added. Final
    	  version of pgjdbc will depend on v1.0
    	- Testing
    	- Probably macros to avoid this Java8-only code propagating to
    	  Java < 8 versions of the driver
    
    * Prepare Java8-only code to be excluded for jre<=8
    
    Actually implemented via macros by JDBC < 4.2.
    
    * On HEAD all testing connections will use SCRAM
    
    * Better error message for drivers w/o SCRAM support
    
    * added configuration for shaded jar
    remove any extraneous classes picked up in the shade plugin


.travis.yml | 3 +- pgjdbc/pom.xml | 60 +++++++- .../postgresql/core/v3/ConnectionFactoryImpl.java | 35 +++++ .../postgresql/jre8/sasl/ScramAuthenticator.java | 166 +++++++++++++++++++++ 4 files changed, 262 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0b11bc35..5de16385 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,8 @@ before_script: - test "x$XA" == 'x' || ./.travis/travis_configure_xa.sh - test "x$REPLICATION" == 'x' || ./.travis/travis_configure_replication.sh - ./.travis/travis_start_postgres.sh - - psql -U postgres -c "create user test with password 'test';" + - test "x$PG_VERSION" != 'xHEAD' || psql -U postgres -c "set password_encryption='scram-sha-256'; create user test with password 'test';" + - test "x$PG_VERSION" = 'xHEAD' || psql -U postgres -c "create user test with password 'test';" - test "x$REPLICATION" == 'x' || psql -U postgres -c "alter user test with replication;" - psql -c 'create database test owner test;' -U postgres - echo "MAVEN_OPTS='-Xmx1g -Dgpg.skip=true'" > ~/.mavenrc diff --git a/pgjdbc/pom.xml b/pgjdbc/pom.xml index ac72cef0..9f74a373 100644 --- a/pgjdbc/pom.xml +++ b/pgjdbc/pom.xml @@ -36,6 +36,14 @@ <checkstyle.version>7.8.2</checkstyle.version> </properties> + <dependencies> + <dependency> + <groupId>com.ongres.scram</groupId> + <artifactId>client</artifactId> + <version>1.0.0-beta.2</version> + </dependency> + </dependencies> + <profiles> <profile> <id>translate</id> @@ -249,6 +257,56 @@ <outputEncoding>UTF-8</outputEncoding> </configuration> </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-shade-plugin</artifactId> + <version>3.1.0</version> + <configuration> + <minimizeJar>true</minimizeJar> + <filters> + <filter> + <artifact>com.ongres.scram:client</artifact> + <includes> + <include>*.*</include> + </includes> + </filter> + <filter> + <artifact>com.github.dblock.waffle:waffle-jna</artifact> + <excludes> + <exclude>**</exclude> + </excludes> + </filter> + <filter> + <artifact>org.slf4j:jcl-over-slf4j</artifact> + <excludes> + <exclude>**</exclude> + </excludes> + </filter> + <filter> + <artifact>*:*</artifact> + <excludes> + <exclude>com/sun/jna/**</exclude> + </excludes> + </filter> + </filters> + </configuration> + <executions> + <execution> + <phase>package</phase> + <goals> + <goal>shade</goal> + </goals> + <configuration> + <relocations> + <relocation> + <pattern>com.ongres</pattern> + <shadedPattern>org.postgresql.shaded.com.ongres</shadedPattern> + </relocation> + </relocations> + </configuration> + </execution> + </executions> + </plugin> </plugins> <pluginManagement> @@ -266,7 +324,7 @@ </dependencies> <configuration> <configLocation>src/main/checkstyle/checks.xml</configLocation> - <suppressionsLocation>src/main/checkstyle/suppressions.xml</suppressionsLocation> + <suppressionsLocation>src/main/checkstyle/suppressions.xml</suppressionsLocation> <violationSeverity>error</violationSeverity> <failOnViolation>true</failOnViolation> <failsOnError>true</failsOnError> 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 3ccfc913..92da28d6 100644 --- a/pgjdbc/src/main/java/org/postgresql/core/v3/ConnectionFactoryImpl.java +++ b/pgjdbc/src/main/java/org/postgresql/core/v3/ConnectionFactoryImpl.java @@ -39,6 +39,7 @@ import java.util.Properties; import java.util.TimeZone; import java.util.logging.Level; import java.util.logging.Logger; + import javax.net.SocketFactory; /** @@ -59,6 +60,9 @@ public class ConnectionFactoryImpl extends ConnectionFactory { private static final int AUTH_REQ_GSS = 7; private static final int AUTH_REQ_GSS_CONTINUE = 8; private static final int AUTH_REQ_SSPI = 9; + private static final int AUTH_REQ_SASL = 10; + private static final int AUTH_REQ_SASL_CONTINUE = 11; + private static final int AUTH_REQ_SASL_FINAL = 12; /** * Marker exception; thrown when we want to fall back to using V2. @@ -413,6 +417,11 @@ public class ConnectionFactoryImpl extends ConnectionFactory { /* SSPI negotiation state, if used */ ISSPIClient sspiClient = null; + //#if mvn.project.property.postgresql.jdbc.spec >= "JDBC4.2" + /* SCRAM authentication state, if used */ + org.postgresql.jre8.sasl.ScramAuthenticator scramAuthenticator = null; + //#endif + try { authloop: while (true) { int beresp = pgStream.receiveChar(); @@ -604,6 +613,32 @@ public class ConnectionFactoryImpl extends ConnectionFactory { sspiClient.continueSSPI(l_msgLen - 8); break; + case AUTH_REQ_SASL: + LOGGER.log(Level.FINEST, " <=BE AuthenticationSASL"); + + //#if mvn.project.property.postgresql.jdbc.spec >= "JDBC4.2" + scramAuthenticator = new org.postgresql.jre8.sasl.ScramAuthenticator(user, password, pgStream); + scramAuthenticator.processServerMechanismsAndInit(); + scramAuthenticator.sendScramClientFirstMessage(); + //#else + if (true) { + throw new PSQLException(GT.tr( + "SCRAM authentication is not supported by this driver. You need JDK >= 8 and pgjdbc >= 42.2.0 (not \".jre\" vesions)", + areq), PSQLState.CONNECTION_REJECTED); + } + //#endif + break; + + //#if mvn.project.property.postgresql.jdbc.spec >= "JDBC4.2" + case AUTH_REQ_SASL_CONTINUE: + scramAuthenticator.processServerFirstMessage(l_msgLen - 4 - 4); + break; + + case AUTH_REQ_SASL_FINAL: + scramAuthenticator.verifyServerSignature(l_msgLen - 4 - 4); + break; + //#endif + case AUTH_REQ_OK: /* Cleanup after successful authentication */ LOGGER.log(Level.FINEST, " <=BE AuthenticationOk"); diff --git a/pgjdbc/src/main/java/org/postgresql/jre8/sasl/ScramAuthenticator.java b/pgjdbc/src/main/java/org/postgresql/jre8/sasl/ScramAuthenticator.java new file mode 100644 index 00000000..87daa05a --- /dev/null +++ b/pgjdbc/src/main/java/org/postgresql/jre8/sasl/ScramAuthenticator.java @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2017, PostgreSQL Global Development Group + * See the LICENSE file in the project root for more information. + */ + + +package org.postgresql.jre8.sasl; + +import org.postgresql.core.PGStream; +import org.postgresql.util.GT; +import org.postgresql.util.PSQLException; +import org.postgresql.util.PSQLState; + +import com.ongres.scram.client.ScramClient; +import com.ongres.scram.client.ScramSession; +import com.ongres.scram.common.exception.ScramException; +import com.ongres.scram.common.exception.ScramInvalidServerSignatureException; +import com.ongres.scram.common.exception.ScramParseException; +import com.ongres.scram.common.exception.ScramServerErrorException; +import com.ongres.scram.common.stringprep.StringPreparations; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class ScramAuthenticator { + private static final Logger LOGGER = Logger.getLogger(ScramAuthenticator.class.getName()); + + private final String user; + private final String password; + private final PGStream pgStream; + private ScramClient scramClient; + private ScramSession scramSession; + private ScramSession.ServerFirstProcessor serverFirstProcessor; + private ScramSession.ClientFinalProcessor clientFinalProcessor; + + @FunctionalInterface + private interface BodySender { + void sendBody(PGStream pgStream) throws IOException; + } + + private void sendAuthenticationMessage(int bodyLength, BodySender bodySender) + throws IOException { + pgStream.sendChar('p'); + pgStream.sendInteger4(Integer.BYTES + bodyLength); + bodySender.sendBody(pgStream); + pgStream.flush(); + } + + public ScramAuthenticator(String user, String password, PGStream pgStream) { + this.user = user; + this.password = password; + this.pgStream = pgStream; + } + + public void processServerMechanismsAndInit() throws IOException, PSQLException { + List<String> mechanisms = new ArrayList<>(); + do { + mechanisms.add(pgStream.receiveString()); + } while (pgStream.peekChar() != 0); + int c = pgStream.receiveChar(); + assert c == 0; + if (mechanisms.size() < 1) { + throw new PSQLException( + GT.tr("No SCRAM mechanism(s) advertised by the server"), + PSQLState.CONNECTION_REJECTED + ); + } + + try { + scramClient = ScramClient + .channelBinding(ScramClient.ChannelBinding.NO) + .stringPreparation(StringPreparations.NO_PREPARATION) + .selectMechanismBasedOnServerAdvertised(mechanisms.toArray(new String[]{})) + .setup(); + } catch (IllegalArgumentException e) { + throw new PSQLException( + GT.tr("Invalid or unsupported by client SCRAM mechanisms", e), + PSQLState.CONNECTION_REJECTED + ); + } + LOGGER.log(Level.FINEST, " Using SCRAM mechanism {0}", scramClient.getScramMechanism().getName()); + + scramSession = + scramClient.scramSession("*"); // Real username is ignored by server, uses startup one + } + + public void sendScramClientFirstMessage() throws IOException { + String clientFirstMessage = scramSession.clientFirstMessage(); + LOGGER.log(Level.FINEST, " FE=> SASLInitialResponse( {0} )", clientFirstMessage); + + String scramMechanismName = scramClient.getScramMechanism().getName(); + byte[] scramMechanismNameBytes = scramMechanismName.getBytes(StandardCharsets.UTF_8); + byte[] clientFirstMessageBytes = clientFirstMessage.getBytes(StandardCharsets.UTF_8); + sendAuthenticationMessage( + (scramMechanismNameBytes.length + 1) + 4 + clientFirstMessageBytes.length, + s -> { + s.send(scramMechanismNameBytes); + s.sendChar(0); // List terminated in '\0' + s.sendInteger4(clientFirstMessageBytes.length); + s.send(clientFirstMessageBytes); + } + ); + } + + public void processServerFirstMessage(int length) throws IOException, PSQLException { + String serverFirstMessage = pgStream.receiveString(length); + LOGGER.log(Level.FINEST, " <=BE AuthenticationSASLContinue( {0} )", serverFirstMessage); + + try { + serverFirstProcessor = scramSession.receiveServerFirstMessage(serverFirstMessage); + } catch (ScramException e) { + throw new PSQLException( + GT.tr("Invalid server-first-message: {0}", serverFirstMessage), + PSQLState.CONNECTION_REJECTED, + e + ); + } + LOGGER.log(Level.FINEST, + " <=BE AuthenticationSASLContinue(salt={0}, iterations={1})", + new Object[] { serverFirstProcessor.getSalt(), serverFirstProcessor.getIteration() } + ); + + clientFinalProcessor = serverFirstProcessor.clientFinalProcessor(password); + + String clientFinalMessage = clientFinalProcessor.clientFinalMessage(); + LOGGER.log(Level.FINEST, " FE=> SASLResponse( {0} )", clientFinalMessage); + + byte[] clientFinalMessageBytes = clientFinalMessage.getBytes(StandardCharsets.UTF_8); + sendAuthenticationMessage( + clientFinalMessageBytes.length, + s -> s.send(clientFinalMessageBytes) + ); + } + + public void verifyServerSignature(int length) throws IOException, PSQLException { + String serverFinalMessage = pgStream.receiveString(length); + LOGGER.log(Level.FINEST, " <=BE AuthenticationSASLFinal( {0} )", serverFinalMessage); + + try { + clientFinalProcessor.receiveServerFinalMessage(serverFinalMessage); + } catch (ScramParseException e) { + throw new PSQLException( + GT.tr("Invalid server-final-message: {0}", serverFinalMessage), + PSQLState.CONNECTION_REJECTED, + e + ); + } catch (ScramServerErrorException e) { + throw new PSQLException( + GT.tr("SCRAM authentication failed, server returned error: {0}", + e.getError().getErrorMessage()), + PSQLState.CONNECTION_REJECTED, + e + ); + } catch (ScramInvalidServerSignatureException e) { + throw new PSQLException( + GT.tr("Invalid server SCRAM signature"), + PSQLState.CONNECTION_REJECTED, + e + ); + } + } +} [parent: 279fb435b392]