org.herodbx.jre7.sasl.HeroScramClient Maven / Gradle / Ivy
/*
 * 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 org.herodbx.jre7.sasl;
import static org.herodbx.scram100b2.common.util.Preconditions.*;
import org.herodbx.herossl.HeroSP;
import org.herodbx.scram100b2.common.ScramMechanism;
import org.herodbx.scram100b2.common.ScramMechanisms;
import org.herodbx.scram100b2.common.gssapi.Gs2CbindFlag;
import org.herodbx.scram100b2.common.stringprep.StringPreparation;
import org.herodbx.scram100b2.common.util.CryptoUtil;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
/**
 * A class that can be parametrized to generate {@link HeroScramSession}s.
 * This class supports the channel binding and string preparation mechanisms that are provided by module scram-common.
 * 
 * The class is fully configurable, including options to selected the desired channel binding,
 * automatically pick the best client SCRAM mechanism based on those supported (advertised) by the server,
 * selecting an externally-provided SecureRandom instance or an external nonceProvider, or choosing the nonce length.
 * 
 * This class is thread-safe if the two following conditions are met:
 * 
 *     - The SecureRandom used ({@link SecureRandom} by default) are thread-safe too.
 *         The contract of {@link java.util.Random} marks it as thread-safe, so inherited classes are also expected
 *         to maintain it.
 *     
 
 *     - No external nonceSupplier is provided; or if provided, it is thread-safe.
 
 * 
 * So this class, once instantiated via the {@link Builder#setup()}} method, can serve for multiple users and
 * authentications.
 */
public class HeroScramClient {
  private static Logger LOGGER = Logger.getLogger(HeroScramClient.class.getName());
  /**
   * Length (in characters, bytes) of the nonce generated by default (if no nonce supplier is provided)
   */
  public static final int DEFAULT_NONCE_LENGTH = 24;
  /**
   * Select whether this client will support channel binding or not
   */
  public enum ChannelBinding {
    /**
     * Don't use channel binding. Server must support at least one non-channel binding mechanism.
     */
    NO(Gs2CbindFlag.CLIENT_NOT),
    /**
     * Force use of channel binding. Server must support at least one channel binding mechanism.
     * Channel binding data will need to be provided as part of the ClientFirstMessage.
     */
    YES(Gs2CbindFlag.CHANNEL_BINDING_REQUIRED),
    /**
     * Channel binding is preferred. Non-channel binding mechanisms will be used if either the server does not
     * support channel binding, or no channel binding data is provided as part of the ClientFirstMessage
     */
    IF_SERVER_SUPPORTS_IT(Gs2CbindFlag.CLIENT_YES_SERVER_NOT);
    private final Gs2CbindFlag gs2CbindFlag;
    ChannelBinding(Gs2CbindFlag gs2CbindFlag) {
      this.gs2CbindFlag = gs2CbindFlag;
    }
    public Gs2CbindFlag gs2CbindFlag() {
      return gs2CbindFlag;
    }
  }
  private final ChannelBinding channelBinding;
  private final StringPreparation stringPreparation;
  private final ScramMechanism scramMechanism;
  private final SecureRandom secureRandom;
  private final Supplier nonceSupplier;
  private HeroScramClient(
    ChannelBinding channelBinding, StringPreparation stringPreparation,
    Optional nonChannelBindingMechanism, Optional channelBindingMechanism,
    SecureRandom secureRandom, Supplier nonceSupplier
  ) {
    assert null != channelBinding : "channelBinding";
    assert null != stringPreparation : "stringPreparation";
    assert nonChannelBindingMechanism.isPresent() || channelBindingMechanism.isPresent()
      : "Either a channel-binding or a non-binding mechanism must be present";
    assert null != secureRandom : "secureRandom";
    assert null != nonceSupplier : "nonceSupplier";
    this.channelBinding = channelBinding;
    this.stringPreparation = stringPreparation;
    this.scramMechanism = nonChannelBindingMechanism.orElseGet(() -> channelBindingMechanism.get());
    this.secureRandom = secureRandom;
    this.nonceSupplier = nonceSupplier;
  }
  /**
   * Selects for the client whether to use channel binding.
   * Refer to {@link ChannelBinding} documentation for the description of the possible values.
   *
   * @param channelBinding The channel binding setting
   * @return The next step in the chain (PreBuilder1).
   * @throws IllegalArgumentException If channelBinding is null
   */
  public static PreBuilder1 channelBinding(ChannelBinding channelBinding) throws IllegalArgumentException {
    return new PreBuilder1(checkNotNull(channelBinding, "channelBinding"));
  }
  /**
   * This class is not meant to be used directly.
   * Use {@link HeroScramClient#channelBinding(ChannelBinding)} instead.
   */
  public static class PreBuilder1 {
    protected final ChannelBinding channelBinding;
    private PreBuilder1(ChannelBinding channelBinding) {
      this.channelBinding = channelBinding;
    }
    /**
     * Selects the string preparation algorithm to use by the client.
     *
     * @param stringPreparation The string preparation algorithm
     * @throws IllegalArgumentException If stringPreparation is null
     */
    public PreBuilder2 stringPreparation(StringPreparation stringPreparation) throws IllegalArgumentException {
      return new PreBuilder2(channelBinding, checkNotNull(stringPreparation, "stringPreparation"));
    }
  }
  /**
   * This class is not meant to be used directly.
   * Use {@link HeroScramClient#channelBinding(ChannelBinding)}.{#stringPreparation(StringPreparation)} instead.
   */
  public static class PreBuilder2 extends PreBuilder1 {
    protected final StringPreparation stringPreparation;
    protected Optional nonChannelBindingMechanism = Optional.empty();
    protected Optional channelBindingMechanism = Optional.empty();
    private PreBuilder2(ChannelBinding channelBinding, StringPreparation stringPreparation) {
      super(channelBinding);
      this.stringPreparation = stringPreparation;
    }
    /**
     * Inform the client of the SCRAM mechanisms supported by the server.
     * Based on this list, the channel binding settings previously specified,
     * and the relative strength of the supported SCRAM mechanisms for this client,
     * the client will have enough data to select which mechanism to use for future interactions with the server.
     * All names provided here need to be standar IANA Registry names for SCRAM mechanisms, or will be ignored.
     *
     * @param serverMechanisms One or more IANA-registered SCRAM mechanism names, as advertised by the server
     * @throws IllegalArgumentException If no server mechanisms are provided
     * @see 
     * SASL SCRAM Family Mechanisms
     */
    public Builder selectMechanismBasedOnServerAdvertised(String... serverMechanisms) {
      checkArgument(null != serverMechanisms && serverMechanisms.length > 0, "serverMechanisms");
      nonChannelBindingMechanism = HeroScramMechanisms.selectMatchingMechanism(false, serverMechanisms);
      boolean b = channelBinding == ChannelBinding.NO;
      boolean b1 = !nonChannelBindingMechanism.isPresent();
      if (b && b1) {
        throw new IllegalArgumentException("Server does not support non channel binding mechanisms");
      }
      channelBindingMechanism = ScramMechanisms.selectMatchingMechanism(true, serverMechanisms);
      if (channelBinding == ChannelBinding.YES && !channelBindingMechanism.isPresent()) {
        throw new IllegalArgumentException("Server does not support channel binding mechanisms");
      }
      if (!(channelBindingMechanism.isPresent() || nonChannelBindingMechanism.isPresent())) {
        throw new IllegalArgumentException("There are no matching mechanisms between client and server");
      }
      return new Builder(channelBinding, stringPreparation, nonChannelBindingMechanism, channelBindingMechanism);
    }
    /**
     * Inform the client of the SCRAM mechanisms supported by the server.
     * Calls {@link Builder#selectMechanismBasedOnServerAdvertised(String...)}
     * with the results of splitting the received comma-separated values.
     *
     * @param serverMechanismsCsv A CSV (Comma-Separated Values) String, containining all the SCRAM mechanisms
     *                            supported by the server
     * @throws IllegalArgumentException If selectMechanismBasedOnServerAdvertisedCsv is null
     */
    public Builder selectMechanismBasedOnServerAdvertisedCsv(String serverMechanismsCsv)
      throws IllegalArgumentException {
      return selectMechanismBasedOnServerAdvertised(
        checkNotNull(serverMechanismsCsv, "serverMechanismsCsv").split(",")
      );
    }
    /**
     * Select a fixed client mechanism. It must be compatible with the channel binding selection previously
     * performed. If automatic selection based on server advertised mechanisms is preferred, please use methods
     * {@link Builder#selectMechanismBasedOnServerAdvertised(String...)} or
     * {@link Builder#selectMechanismBasedOnServerAdvertisedCsv(String)}.
     *
     * @param scramMechanism The selected scram mechanism
     * @throws IllegalArgumentException If the selected mechanism is null or not compatible with the prior
     *                                  channel binding selection,
     *                                  or channel binding selection is dependent on the server advertised methods
     */
    public Builder selectClientMechanism(ScramMechanism scramMechanism) {
      checkNotNull(scramMechanism, "scramMechanism");
      if (channelBinding == ChannelBinding.IF_SERVER_SUPPORTS_IT) {
        throw new IllegalArgumentException(
          "If server selection is considered, no direct client selection should be performed"
        );
      }
      if (
        channelBinding == ChannelBinding.YES && !scramMechanism.supportsChannelBinding()
          ||
          channelBinding == ChannelBinding.NO && scramMechanism.supportsChannelBinding()
      ) {
        throw new IllegalArgumentException("Incompatible selection of mechanism and channel binding");
      }
      if (scramMechanism.supportsChannelBinding()) {
        return new Builder(channelBinding, stringPreparation, Optional.empty(), Optional.of(scramMechanism));
      } else {
        return new Builder(channelBinding, stringPreparation, Optional.of(scramMechanism), Optional.empty());
      }
    }
  }
  /**
   * This class is not meant to be used directly.
   * Use instead {@link HeroScramClient#channelBinding(ChannelBinding)} and chained methods.
   */
  public static class Builder extends PreBuilder2 {
    private final Optional nonChannelBindingMechanism;
    private final Optional channelBindingMechanism;
    private SecureRandom secureRandom = new SecureRandom();
    private Supplier nonceSupplier;
    private int nonceLength = DEFAULT_NONCE_LENGTH;
    private Builder(
      ChannelBinding channelBinding, StringPreparation stringPreparation,
      Optional nonChannelBindingMechanism, Optional channelBindingMechanism
    ) {
      super(channelBinding, stringPreparation);
      this.nonChannelBindingMechanism = nonChannelBindingMechanism;
      this.channelBindingMechanism = channelBindingMechanism;
    }
    /**
     * Optional call. Selects a non-default SecureRandom instance,
     * based on the given algorithm and optionally provider.
     * This SecureRandom instance will be used to generate secure random values,
     * like the ones required to generate the nonce
     * (unless an external nonce provider is given via {@link Builder#nonceSupplier(Supplier)}).
     * 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;
    }
    /**
     * Optional call. The client will use a default nonce generator,
     * unless an external one is provided by this method.         *
     *
     * @param nonceSupplier A supplier of valid nonce Strings.
     *                      Please note that according to the
     *                      SCRAM RFC
     *                      only ASCII printable characters (except the comma, ',') are permitted on a nonce.
     *                      Length is not limited.
     * @return The same class
     * @throws IllegalArgumentException If nonceSupplier is null
     */
    public Builder nonceSupplier(Supplier nonceSupplier) throws IllegalArgumentException {
      this.nonceSupplier = checkNotNull(nonceSupplier, "nonceSupplier");
      return this;
    }
    /**
     * Sets a non-default ({@link HeroScramClient#DEFAULT_NONCE_LENGTH}) length for the nonce generation,
     * if no alternate nonceSupplier is provided via {@link Builder#nonceSupplier(Supplier)}.
     *
     * @param length The length of the nonce. Must be positive and greater than 0
     * @return The same class
     * @throws IllegalArgumentException If length is less than 1
     */
    public Builder nonceLength(int length) throws IllegalArgumentException {
      this.nonceLength = gt0(length, "length");
      return this;
    }
    /**
     * Gets the client, fully constructed and configured, with the provided channel binding, string preparation
     * properties, and the selected SCRAM mechanism based on server supported mechanisms.
     * If no SecureRandom algorithm and provider were provided, a default one would be used.
     * If no nonceSupplier was provided, a default nonce generator would be used,
     * of the {@link HeroScramClient#DEFAULT_NONCE_LENGTH} length, unless {@link Builder#nonceLength(int)} is called.
     *
     * @return The fully built instance.
     */
    public HeroScramClient setup() {
      return new HeroScramClient(
        channelBinding, stringPreparation, nonChannelBindingMechanism, channelBindingMechanism,
        secureRandom,
        nonceSupplier != null ? nonceSupplier : () -> CryptoUtil.nonce(nonceLength, secureRandom)
      );
    }
  }
  public StringPreparation getStringPreparation() {
    return stringPreparation;
  }
  public ScramMechanism getScramMechanism() {
    return scramMechanism;
  }
  /**
   * List all the supported SCRAM mechanisms by this client implementation
   *
   * @return A list of the IANA-registered, SCRAM supported mechanisms
   */
  public static List supportedMechanisms() {
    return Arrays.stream(ScramMechanisms.values()).map(m -> m.getName()).collect(Collectors.toList());
  }
  /**
   * Instantiates a {@link HeroScramSession} for the specified user and this parametrized generator.
   *
   * @param user The username of the authentication exchange
   * @return The ScramSession instance
   */
  public HeroScramSession scramSession(String user) {
    String nonce = nonceSupplier.get();
    if ("scram-gm-256".equalsIgnoreCase(scramMechanism.getName())) {
      LOGGER.log(Level.INFO, "scramMechanism: scram-gm-256");
      LOGGER.log(Level.FINE, "nonce(外部传入):[{0}]", nonce);
      nonce = HeroSP.genRandomPrintable(nonce.length(), 0x2c);//0x2c 是英文逗号,pg官方点名不用。顾法华,2020年10月16日10:40:14
//      nonce = ",Xd%If#l.JW|'3##KwJT6\\`d";//报错
//      nonce = ",Xd%If#l.JW|'3##KwJT6x`d";//报错
//      nonce = ",Xd-If#l.JW|'3##KwJT6x`d";//报错
//      nonce = ",Xd-If#l.JW|'3##KwJT6x0d";//报错
//      nonce = "kXd-If#l.JW|'3##KwJT6x0d";
//      nonce = "\"Xd%If#l.JW|'3##KwJT6\\`d";
//      nonce = ",X=1If#l.JW|'3##KwJT6x0d";//报错,并蒙混进入后续操作
      LOGGER.log(Level.FINE, "nonce(加密平台重新生成):[{0}]", nonce);
    }
    return new HeroScramSession(scramMechanism, stringPreparation, checkNotEmpty(user, "user"), nonce);
  }
}
                 © 2015 - 2025 Weber Informatics LLC | Privacy Policy