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

Add SCRAM-SHA-256 support #842

Merged
merged 5 commits into from
Nov 21, 2017
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
60 changes: 59 additions & 1 deletion pgjdbc/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this be a beta dependency?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That bothers me a bit as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was planning on bumping it to 1.0.0 when it is well tested and ready to be integrated. So if you feel this is OK, I will proceed and release 1.0.0 and update the PR.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ahachete well, it will never be tested if it's not included in pgjdbc, so is a chicken-egg problem, it's your code, so you better know if it is well tested.

I'm Ok if it's released as a Beta feature, mark it as such in release notes and if everything goes fine, bumping it to 1.0.0 and update it for the next release cycle.

</dependency>
</dependencies>

<profiles>
<profile>
<id>translate</id>
Expand Down Expand Up @@ -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>
Expand All @@ -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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import java.util.TimeZone;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.net.SocketFactory;

/**
Expand All @@ -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.
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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");
Expand Down
166 changes: 166 additions & 0 deletions pgjdbc/src/main/java/org/postgresql/jre8/sasl/ScramAuthenticator.java
Original file line number Diff line number Diff line change
@@ -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
);
}
}
}