com.impossibl.postgres.protocol.sasl.scram.client.ScramSessionFactory Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of pgjdbc-ng Show documentation
Show all versions of pgjdbc-ng Show documentation
PostgreSQL JDBC - NG - Driver
The newest version!
/*
* Copyright 2017, OnGres.
*
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
* following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
* disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
* following disclaimer in the documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
*/
package com.impossibl.postgres.protocol.sasl.scram.client;
import com.impossibl.postgres.protocol.sasl.scram.ScramMechanism;
import com.impossibl.postgres.protocol.sasl.scram.ScramMechanisms;
import com.impossibl.postgres.protocol.sasl.scram.exception.ScramException;
import com.impossibl.postgres.protocol.sasl.scram.stringprep.StringPreparation;
import com.impossibl.postgres.protocol.sasl.scram.stringprep.StringPreparations;
import com.impossibl.postgres.protocol.sasl.scram.util.CryptoUtil;
import static com.impossibl.postgres.protocol.sasl.scram.util.Preconditions.checkNotNull;
import static com.impossibl.postgres.protocol.sasl.scram.util.Preconditions.gt0;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SecureRandom;
import java.util.Collection;
import java.util.Collections;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Stream;
import static java.lang.String.format;
import static java.util.Comparator.comparingInt;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;
/**
* A factory to generate properly configured {@link ScramSession} instances. The {@link ScramSessionFactory} itself
* must be instantiated using {@link ScramSessionFactory.Builder} to ensure a properly configured session factory.
*
* {@link ScramSessionFactory.Builder} is a declarative builder that allows consumers to provide only the required
* information (e.g. server advertised mechanisms, channel-binding method, etc.) and ensures they receive a properly
* configured factory with the strongest available authentication flow selected.
*
* Available builder methods for controlling authentication mechanism selection:
*
* -
* {@link ScramSessionFactory.Builder#serverAdvertisedMechanisms} -
* The list of supported mechanisms advertised by the server.
*
* -
* {@link ScramSessionFactory.Builder#channelBindMethod} -
* The selected/supported channel-bind method the client plans to use.
*
* -
* {@link ScramSessionFactory.Builder#preferChannelBindingMechanism(boolean)} -
* Whether the selection process should favor selecting a mechanism that requires channel binding over a method
* that has a stronger algorithm. For example, if 'SCRAM-SHA1-PLUS' and 'SCRAM-SHA256' are the available methods
* should it favor the `SCRAM-SHA1-PLUS` because it requires channel binding.
*
*
*/
public class ScramSessionFactory {
/**
* Default nonce byte length
*/
public static final int DEFAULT_NONCE_LENGTH = 24;
private final ScramMechanism scramMechanism;
private final String channelBindMethod;
private final boolean serverSupportsChannelBinding;
private final StringPreparation stringPreparation;
private final int nonceLength;
private final SecureRandom secureRandom;
/**
* Instantiates a {@link ScramSession} for the specified user with this factory's selected mechanism and
* algorithmic features.
*
* @param user The username of the authentication exchange
* @return The ScramSession instance
*/
public ScramSession start(String user) {
String nonce = CryptoUtil.nonce(nonceLength, secureRandom);
return new ScramSession(
scramMechanism,
channelBindMethod, serverSupportsChannelBinding,
stringPreparation,
checkNotNull(user, "user"), nonce
);
}
private ScramSessionFactory(ScramMechanism scramMechanism, String channelBindMethod, boolean serverSupportsChannelBinding,
StringPreparation stringPreparation, int nonceLength, SecureRandom secureRandom) {
assert null != scramMechanism : "scramMechanism";
assert null != stringPreparation : "stringPreparation";
assert null != secureRandom : "secureRandom";
this.scramMechanism = scramMechanism;
this.channelBindMethod = channelBindMethod;
this.serverSupportsChannelBinding = serverSupportsChannelBinding;
this.stringPreparation = stringPreparation;
this.nonceLength = nonceLength;
this.secureRandom = secureRandom;
}
public static Builder builder() {
try {
return new Builder();
}
catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
public static class Builder {
private Collection serverAdvertisedMechanisms;
private String channelBindMethod;
private boolean preferChannelBindingOverAlgorithmStrength;
private StringPreparation stringPreparation;
private int nonceLength;
private SecureRandom secureRandom;
private Builder() throws NoSuchAlgorithmException {
this.serverAdvertisedMechanisms = Collections.emptyList();
this.channelBindMethod = null;
this.preferChannelBindingOverAlgorithmStrength = false;
this.stringPreparation = StringPreparations.NO_PREPARATION;
this.nonceLength = DEFAULT_NONCE_LENGTH;
this.secureRandom = SecureRandom.getInstanceStrong();
}
/**
* Provide the list of mechanism names advertised by the server. The method will filter out
* mechanisms unsupported by the current implementation.
*
* @param serverAdvertisedMechanisms Names of advertised mechanisms
*/
public Builder serverAdvertisedMechanisms(Collection serverAdvertisedMechanisms) {
checkNotNull(serverAdvertisedMechanisms, "serverAdvertisedMechanisms");
this.serverAdvertisedMechanisms =
serverAdvertisedMechanisms.stream()
.map(ScramMechanisms::byName)
.filter(Objects::nonNull)
.collect(toList());
return this;
}
/**
* Determine whether the selection process should favor selecting a mechanism that requires channel
* binding over a method that has a stronger algorithm. For example, if 'SCRAM-SHA1-PLUS' and 'SCRAM-SHA256'
* are the available methods should it favor the `SCRAM-SHA1-PLUS` because it requires channel binding.
*
* @param preferChannelBinding Should selection prefer a channel binding mechanism
*/
public Builder preferChannelBindingMechanism(boolean preferChannelBinding) {
this.preferChannelBindingOverAlgorithmStrength = preferChannelBinding;
return this;
}
/**
* The selected/supported channel-bind method the client plans to use. If the client does not support any
* channel binding methods it can pass {@code null}.
*
* @param channelBindMethod Selected/supported channel-bind method.
*/
public Builder channelBindMethod(String channelBindMethod) {
this.channelBindMethod = channelBindMethod;
return this;
}
/**
* Optional call. The string preparation method that should be performed on the user's name and password. The
* default is to use {@link StringPreparations#SASL_PREPARATION}.
*
* @param stringPreparation Selected string preparation method.
*/
public Builder stringPreparation(StringPreparation stringPreparation) {
this.stringPreparation = checkNotNull(stringPreparation, "stringPreparation");
return this;
}
/**
* Optional call. The length of randomly generated nonce that should be used. Only required if a specific
* non-default nonce length is required otherwise an implementation defined default length is used.
*
* @param nonceLength Length of randomly generated nonce.
*/
public Builder nonceLength(int nonceLength) {
this.nonceLength = gt0(nonceLength, "nonceLength");
return this;
}
/**
* Optional call. Selects a non-default SecureRandom instance, based on the given algorithm and
* optionally a specific security provider. This selected {@link SecureRandom} instance will be used to
* generate secure random values (e.g. nonces). Algorithm and provider names are those supported by the
* {@link SecureRandom} class.
*
* @param algorithm The name of the algorithm to use.
* @param provider The name of the provider of SecureRandom. Might be null.
* @return The same class
* @throws IllegalArgumentException If algorithm is null, or either the algorithm or provider are not supported
*/
public Builder secureRandomAlgorithmProvider(String algorithm, String provider) throws IllegalArgumentException {
checkNotNull(algorithm, "algorithm");
try {
secureRandom = null == provider ?
SecureRandom.getInstance(algorithm) :
SecureRandom.getInstance(algorithm, provider);
}
catch (NoSuchAlgorithmException | NoSuchProviderException e) {
throw new IllegalArgumentException("Invalid algorithm or provider", e);
}
return this;
}
public ScramSessionFactory build() throws ScramException {
// If channel binding is supported, find best mechanisms supporting it
Optional selectedChannelBindingScramMechanism = Optional.empty();
if (channelBindMethod != null) {
selectedChannelBindingScramMechanism =
serverAdvertisedMechanisms.stream()
.filter(ScramMechanism::requiresChannelBinding)
.max(comparingInt(ScramMechanism::algorithmKeyLength));
}
// Find best non binding mechanism as well
Optional selectedNonChannelBindingScramMechanism =
serverAdvertisedMechanisms.stream()
.filter(scramMechanism -> !scramMechanism.requiresChannelBinding())
.max(comparingInt(ScramMechanism::algorithmKeyLength));
// Choose best mechanism based on availability and preference
Optional selectedScramMechanism;
if (selectedChannelBindingScramMechanism.isPresent() && selectedNonChannelBindingScramMechanism.isPresent()) {
if (preferChannelBindingOverAlgorithmStrength) {
// Choose the channel binding mechanism
selectedScramMechanism = selectedChannelBindingScramMechanism;
}
else {
// Choose based on key length
selectedScramMechanism =
Stream.of(selectedChannelBindingScramMechanism.get(), selectedNonChannelBindingScramMechanism.get())
.max(comparingInt(ScramMechanism::algorithmKeyLength));
}
}
else if (selectedChannelBindingScramMechanism.isPresent()) {
selectedScramMechanism = selectedChannelBindingScramMechanism;
}
else {
selectedScramMechanism = selectedNonChannelBindingScramMechanism;
}
if (!selectedScramMechanism.isPresent()) {
String algorithmNames = serverAdvertisedMechanisms.stream().map(ScramMechanism::getName).collect(joining());
throw new ScramException(format("Unable to negotiate supported mechanism (advertised %s)", algorithmNames));
}
return new ScramSessionFactory(
selectedScramMechanism.get(), channelBindMethod,
selectedChannelBindingScramMechanism.isPresent(), stringPreparation,
nonceLength, secureRandom
);
}
}
}