Skip to content
Permalink
Browse files

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
  • Loading branch information
ahachete authored and davecramer committed Nov 21, 2017
1 parent 279fb43 commit befea18d153dda7814daef4e036d3f5daf8de1e5
@@ -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
@@ -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>
@@ -39,6 +39,7 @@
import java.util.TimeZone;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.net.SocketFactory;

/**
@@ -59,6 +60,9 @@
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 @@ private void doAuthentication(PGStream pgStream, String host, String user, Prope
/* 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 @@ private void doAuthentication(PGStream pgStream, String host, String user, Prope
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");
@@ -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
);
}
}
}

0 comments on commit befea18

Please sign in to comment.
You can’t perform that action at this time.