content: /*
   * Copyright (c) 2015, 2022, Oracle and/or its affiliates.
   *
   * This program is free software; you can redistribute it and/or modify it under
   * the terms of the GNU General Public License, version 2.0, as published by the
   * Free Software Foundation.
   *
   * This program is also distributed with certain software (including but not
   * limited to OpenSSL) that is licensed under separate terms, as designated in a
   * particular file or component or in included license documentation. The
   * authors of MySQL hereby grant you an additional permission to link the
   * program and your derivative works with the separately licensed software that
   * they have included with MySQL.
   *
   * Without limiting anything contained in the foregoing, this file, which is
   * part of MySQL Connector/J, is also subject to the Universal FOSS Exception,
   * version 1.0, a copy of which can be found at
   * http://oss.oracle.com/licenses/universal-foss-exception.
   *
   * This program is distributed in the hope that it will be useful, but WITHOUT
   * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
   * FOR A PARTICULAR PURPOSE. See the GNU General Public License, version 2.0,
   * for more details.
   *
   * You should have received a copy of the GNU General Public License along with
   * this program; if not, write to the Free Software Foundation, Inc.,
   * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
   */
  
  package com.mysql.cj.protocol.a;
  
  import java.util.ArrayList;
  import java.util.Collections;
  import java.util.HashMap;
  import java.util.LinkedList;
  import java.util.List;
  import java.util.Map;
  
  import com.mysql.cj.Constants;
  import com.mysql.cj.Messages;
  import com.mysql.cj.callback.MysqlCallbackHandler;
  import com.mysql.cj.callback.UsernameCallback;
  import com.mysql.cj.conf.PropertyDefinitions.SslMode;
  import com.mysql.cj.conf.PropertyKey;
  import com.mysql.cj.conf.PropertySet;
  import com.mysql.cj.conf.RuntimeProperty;
  import com.mysql.cj.exceptions.ExceptionFactory;
  import com.mysql.cj.exceptions.ExceptionInterceptor;
  import com.mysql.cj.exceptions.UnableToConnectException;
  import com.mysql.cj.exceptions.WrongArgumentException;
  import com.mysql.cj.protocol.AuthenticationPlugin;
  import com.mysql.cj.protocol.AuthenticationProvider;
  import com.mysql.cj.protocol.Protocol;
  import com.mysql.cj.protocol.ServerSession;
  import com.mysql.cj.protocol.a.NativeConstants.IntegerDataType;
  import com.mysql.cj.protocol.a.NativeConstants.StringLengthDataType;
  import com.mysql.cj.protocol.a.NativeConstants.StringSelfDataType;
  import com.mysql.cj.protocol.a.authentication.AuthenticationFidoClient;
  import com.mysql.cj.protocol.a.authentication.AuthenticationKerberosClient;
  import com.mysql.cj.protocol.a.authentication.AuthenticationLdapSaslClientPlugin;
  import com.mysql.cj.protocol.a.authentication.AuthenticationOciClient;
  import com.mysql.cj.protocol.a.authentication.CachingSha2PasswordPlugin;
  import com.mysql.cj.protocol.a.authentication.MysqlClearPasswordPlugin;
  import com.mysql.cj.protocol.a.authentication.MysqlNativePasswordPlugin;
  import com.mysql.cj.protocol.a.authentication.MysqlOldPasswordPlugin;
  import com.mysql.cj.protocol.a.authentication.Sha256PasswordPlugin;
  import com.mysql.cj.protocol.a.result.OkPacket;
  import com.mysql.cj.util.StringUtils;
  
  public class NativeAuthenticationProvider implements AuthenticationProvider<NativePacketPayload> {
      private static final int AUTH_411_OVERHEAD = 33;
      private static final String NONE = "none";
  
      private String seed;
      private String username;
      private String password;
      private String database;
      private boolean useConnectWithDb;
  
      private ExceptionInterceptor exceptionInterceptor;
      private PropertySet propertySet;
  
      private Protocol<NativePacketPayload> protocol;
  
      /**
       * Contains instances of authentication plugins that implements {@link AuthenticationPlugin} interface. Key values are MySQL protocol plugin names, for
       * example "mysql_native_password" and "mysql_old_password" for built-in plugins.
       */
      private Map<String, AuthenticationPlugin<NativePacketPayload>> authenticationPlugins = null;
      /**
       * Protocol name of default authentication plugin in client
       */
      private String clientDefaultAuthenticationPluginName = null;
      /**
       * Was the client default authentication plugin explicitly set?
       */
      private boolean clientDefaultAuthenticationPluginExplicitelySet = false;
      /**
       * Protocol name of default authentication plugin in server
       */
      private String serverDefaultAuthenticationPluginName = null;
  
      /**
       * A callback for updating the username from the authentication plugin.
       */
      private MysqlCallbackHandler callbackHandler = (cb) -> {
          if (cb instanceof UsernameCallback) {
              this.username = ((UsernameCallback) cb).getUsername();
          }
      };
  
      public NativeAuthenticationProvider() {
      }
  
      @Override
      public void init(Protocol<NativePacketPayload> prot, PropertySet propSet, ExceptionInterceptor excInterceptor) {
          this.protocol = prot;
          this.propertySet = propSet;
          this.exceptionInterceptor = excInterceptor;
      }
  
      /**
       * Initialize communications with the MySQL server. Handles logging on, and
       * handling initial connection errors.
       * 
       * @param user
       *            user name
       * @param pass
       *            password
       * @param db
       *            database name
       */
      @Override
      public void connect(String user, String pass, String db) {
          ServerSession sessState = this.protocol.getServerSession();
          this.username = user;
          this.password = pass;
          this.database = db;
  
          NativeCapabilities capabilities = (NativeCapabilities) sessState.getCapabilities();
          NativePacketPayload buf = capabilities.getInitialHandshakePacket();
  
          SslMode sslMode = this.propertySet.<SslMode>getEnumProperty(PropertyKey.sslMode).getValue();
          int capabilityFlags = capabilities.getCapabilityFlags();
          if (((capabilityFlags & NativeServerSession.CLIENT_SSL) == 0) && sslMode != SslMode.DISABLED && sslMode != SslMode.PREFERRED) {
              // check SSL availability
              throw ExceptionFactory.createException(UnableToConnectException.class, Messages.getString("MysqlIO.15"), getExceptionInterceptor());
          } else if ((capabilityFlags & NativeServerSession.CLIENT_SECURE_CONNECTION) == 0) {
              // TODO: better messaging
              throw ExceptionFactory.createException(UnableToConnectException.class, "CLIENT_SECURE_CONNECTION is required", getExceptionInterceptor());
          } else if ((capabilityFlags & NativeServerSession.CLIENT_PLUGIN_AUTH) == 0) {
              // TODO: better messaging
              throw ExceptionFactory.createException(UnableToConnectException.class, "CLIENT_PLUGIN_AUTH is required", getExceptionInterceptor());
          }
  
          sessState.setStatusFlags(capabilities.getStatusFlags()); // read status flags (2 bytes)
          int authPluginDataLength = capabilities.getAuthPluginDataLength();
  
          StringBuilder fullSeed = new StringBuilder(authPluginDataLength > 0 ? authPluginDataLength : NativeConstants.SEED_LENGTH);
          fullSeed.append(capabilities.getSeed()); // read auth-plugin-data-part-1 (string[8])
          fullSeed.append(authPluginDataLength > 0 ?  // read string[$len] auth-plugin-data-part-2 ($len=MAX(13, length of auth-plugin-data - 8))
                  buf.readString(StringLengthDataType.STRING_FIXED, "ASCII", authPluginDataLength - 8) : buf.readString(StringSelfDataType.STRING_TERM, "ASCII"));
          this.seed = fullSeed.toString();
  
          this.useConnectWithDb = (this.database != null) && (this.database.length() > 0)
                  && !this.propertySet.getBooleanProperty(PropertyKey.createDatabaseIfNotExist).getValue();
  
          long clientParam = capabilityFlags & NativeServerSession.CLIENT_LONG_PASSWORD //
                  | (this.propertySet.getBooleanProperty(PropertyKey.useAffectedRows).getValue() ? //
                          0 : capabilityFlags & NativeServerSession.CLIENT_FOUND_ROWS) //
                  | capabilityFlags & NativeServerSession.CLIENT_LONG_FLAG //
                  | (this.useConnectWithDb ? capabilityFlags & NativeServerSession.CLIENT_CONNECT_WITH_DB : 0) //
                  | (this.propertySet.getBooleanProperty(PropertyKey.useCompression).getValue() ? //
                          capabilityFlags & NativeServerSession.CLIENT_COMPRESS : 0) //
                  | (this.propertySet.getBooleanProperty(PropertyKey.allowLoadLocalInfile).getValue()
                          || this.propertySet.getStringProperty(PropertyKey.allowLoadLocalInfileInPath).isExplicitlySet() ? //
                                  capabilityFlags & NativeServerSession.CLIENT_LOCAL_FILES : 0) //
                  | capabilityFlags & NativeServerSession.CLIENT_PROTOCOL_41 //
                  | (this.propertySet.getBooleanProperty(PropertyKey.interactiveClient).getValue() ? //
                          capabilityFlags & NativeServerSession.CLIENT_INTERACTIVE : 0) //
                  | (this.propertySet.<SslMode>getEnumProperty(PropertyKey.sslMode).getValue() != SslMode.DISABLED ? //
                          capabilityFlags & NativeServerSession.CLIENT_SSL : 0) //
                  | capabilityFlags & NativeServerSession.CLIENT_TRANSACTIONS // Required to get server status values.
                  | NativeServerSession.CLIENT_SECURE_CONNECTION //
                  | (this.propertySet.getBooleanProperty(PropertyKey.allowMultiQueries).getValue() ? //
                          capabilityFlags & NativeServerSession.CLIENT_MULTI_STATEMENTS : 0) //
                  | capabilityFlags & NativeServerSession.CLIENT_MULTI_RESULTS // Always allow multiple result sets.
                  | capabilityFlags & NativeServerSession.CLIENT_PS_MULTI_RESULTS // Always allow multiple result sets for SSPS.
                  | NativeServerSession.CLIENT_PLUGIN_AUTH //
                  | (NONE.equals(this.propertySet.getStringProperty(PropertyKey.connectionAttributes).getValue()) ? //
                          0 : capabilityFlags & NativeServerSession.CLIENT_CONNECT_ATTRS) //
                  | capabilityFlags & NativeServerSession.CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA //
                  | (this.propertySet.getBooleanProperty(PropertyKey.disconnectOnExpiredPasswords).getValue() ? //
                          0 : capabilityFlags & NativeServerSession.CLIENT_CAN_HANDLE_EXPIRED_PASSWORD) //
                  | (this.propertySet.getBooleanProperty(PropertyKey.trackSessionState).getValue() ? // 
                          capabilityFlags & NativeServerSession.CLIENT_SESSION_TRACK : 0) //
                  | capabilityFlags & NativeServerSession.CLIENT_DEPRECATE_EOF //
                  | capabilityFlags & NativeServerSession.CLIENT_QUERY_ATTRIBUTES // 
                  | capabilityFlags & NativeServerSession.CLIENT_MULTI_FACTOR_AUTHENTICATION;
  
          sessState.setClientParam(clientParam);
  
          /* First, negotiate SSL connection */
          if ((clientParam & NativeServerSession.CLIENT_SSL) != 0) {
              this.protocol.negotiateSSLConnection();
          }
  
          if (buf.isOKPacket()) {
              throw ExceptionFactory.createException(Messages.getString("AuthenticationProvider.UnexpectedAuthenticationApproval"), getExceptionInterceptor());
          }
  
          proceedHandshakeWithPluggableAuthentication(buf);
  
          this.password = null;
      }
  
      /**
       * Fill the authentication plugins map.
       * 
       * Starts by filling the map with instances of the built-in authentication plugins. Then creates instances of plugins listed in the "authenticationPlugins"
       * connection property and adds them to the map too.
       * 
       * The key for the map entry is got by {@link AuthenticationPlugin#getProtocolPluginName()} thus it is possible to replace built-in plugins with custom
       * implementations. To do it, the custom plugin should return one of the values "mysql_native_password", "mysql_clear_password", "sha256_password",
       * "caching_sha2_password", "mysql_old_password", "authentication_ldap_sasl_client" or "authentication_kerberos_client" from its own getProtocolPluginName()
       * method.
       */
      @SuppressWarnings("unchecked")
      private void loadAuthenticationPlugins() {
          // default plugin
          RuntimeProperty<String> defaultAuthenticationPluginProp = this.propertySet.getStringProperty(PropertyKey.defaultAuthenticationPlugin);
          String defaultAuthenticationPluginValue = defaultAuthenticationPluginProp.getValue();
          if (defaultAuthenticationPluginValue == null || "".equals(defaultAuthenticationPluginValue.trim())) {
              throw ExceptionFactory.createException(WrongArgumentException.class,
                      Messages.getString("AuthenticationProvider.BadDefaultAuthenticationPlugin", new Object[] { defaultAuthenticationPluginValue }),
                      getExceptionInterceptor());
          }
  
          // disabled plugins
          String disabledPlugins = this.propertySet.getStringProperty(PropertyKey.disabledAuthenticationPlugins).getValue();
          List<String> disabledAuthenticationPlugins;
          if (disabledPlugins != null && !"".equals(disabledPlugins)) {
              disabledAuthenticationPlugins = StringUtils.split(disabledPlugins, ",", true);
          } else {
              disabledAuthenticationPlugins = Collections.EMPTY_LIST;
          }
  
          this.authenticationPlugins = new HashMap<>();
          List<AuthenticationPlugin<NativePacketPayload>> pluginsToInit = new LinkedList<>();
  
          // built-in plugins
          pluginsToInit.add(new MysqlNativePasswordPlugin());
          pluginsToInit.add(new MysqlClearPasswordPlugin());
          pluginsToInit.add(new Sha256PasswordPlugin());
          pluginsToInit.add(new CachingSha2PasswordPlugin());
          pluginsToInit.add(new MysqlOldPasswordPlugin());
          pluginsToInit.add(new AuthenticationLdapSaslClientPlugin());
          pluginsToInit.add(new AuthenticationKerberosClient());
          pluginsToInit.add(new AuthenticationOciClient());
          pluginsToInit.add(new AuthenticationFidoClient());
  
          // plugins from authenticationPluginClasses connection parameter
          String authenticationPluginClasses = this.propertySet.getStringProperty(PropertyKey.authenticationPlugins).getValue();
          if (authenticationPluginClasses != null && !"".equals(authenticationPluginClasses.trim())) {
              List<String> pluginsToCreate = StringUtils.split(authenticationPluginClasses, ",", true);
              for (String className : pluginsToCreate) {
                  try {
                      pluginsToInit.add((AuthenticationPlugin<NativePacketPayload>) Class.forName(className).newInstance());
                  } catch (Throwable t) {
                      throw ExceptionFactory.createException(WrongArgumentException.class,
                              Messages.getString("AuthenticationProvider.BadAuthenticationPlugin", new Object[] { className }), t, this.exceptionInterceptor);
                  }
              }
          }
  
          // add plugin instances
          boolean defaultFound = false;
          for (AuthenticationPlugin<NativePacketPayload> plugin : pluginsToInit) {
              String pluginProtocolName = plugin.getProtocolPluginName();
              String pluginClassName = plugin.getClass().getName();
              boolean disabledByProtocolName = disabledAuthenticationPlugins.contains(pluginProtocolName);
              boolean disabledByClassName = disabledAuthenticationPlugins.contains(pluginClassName);
  
              if (disabledByProtocolName || disabledByClassName) {
                  // check if the default plugin is disabled
                  if (!defaultFound
                          && (defaultAuthenticationPluginValue.equals(pluginProtocolName) || defaultAuthenticationPluginValue.equals(pluginClassName))) {
                      throw ExceptionFactory.createException(WrongArgumentException.class,
                              Messages.getString("AuthenticationProvider.BadDisabledAuthenticationPlugin",
                                      new Object[] { disabledByClassName ? pluginClassName : pluginProtocolName }),
                              getExceptionInterceptor());
                  }
              } else {
                  this.authenticationPlugins.put(pluginProtocolName, plugin);
                  if (!defaultFound
                          && (defaultAuthenticationPluginValue.equals(pluginProtocolName) || defaultAuthenticationPluginValue.equals(pluginClassName))) {
                      this.clientDefaultAuthenticationPluginName = pluginProtocolName;
                      this.clientDefaultAuthenticationPluginExplicitelySet = defaultAuthenticationPluginProp.isExplicitlySet();
                      defaultFound = true;
                  }
              }
          }
  
          // check if the default plugin is listed
          if (!defaultFound) {
              throw ExceptionFactory.createException(WrongArgumentException.class,
                      Messages.getString("AuthenticationProvider.DefaultAuthenticationPluginIsNotListed", new Object[] { defaultAuthenticationPluginValue }),
                      getExceptionInterceptor());
          }
      }
  
      /**
       * Get an authentication plugin instance from the authentication plugins map by pluginName key. If such plugin is found, its method
       * {@link AuthenticationPlugin#isReusable()} is called and if the value returned is <code>false</code> then a new instance of the plugin is returned
       * otherwise the instance that already exists is returned.
       * 
       * If plugin is not found method returns null, in such case the subsequent behavior
       * of handshake process depends on type of last packet received from server:
       * if it was Auth Challenge Packet then handshake will proceed with default plugin,
       * if it was Auth Method Switch Request Packet then handshake will be interrupted with exception.
       * 
       * @param pluginName
       *            mysql protocol plugin names, for example "mysql_native_password" and "mysql_old_password" for built-in plugins
       * @return null if plugin is not found or authentication plugin instance initialized with current connection properties
       */
      @SuppressWarnings("unchecked")
      private AuthenticationPlugin<NativePacketPayload> getAuthenticationPlugin(String pluginName) {
          AuthenticationPlugin<NativePacketPayload> plugin = this.authenticationPlugins.get(pluginName);
  
          if (plugin == null) {
              return null;
          }
  
          if (!plugin.isReusable()) {
              try {
                  plugin = plugin.getClass().newInstance();
              } catch (Throwable t) {
                  throw ExceptionFactory.createException(WrongArgumentException.class,
                          Messages.getString("AuthenticationProvider.BadAuthenticationPlugin", new Object[] { plugin.getClass().getName() }), t,
                          getExceptionInterceptor());
              }
          }
  
          plugin.init(this.protocol, this.callbackHandler);
          return plugin;
      }
  
      /**
       * Check if given plugin requires confidentiality, but connection is without SSL
       * 
       * @param plugin
       *            {@link AuthenticationPlugin}
       */
      private void checkConfidentiality(AuthenticationPlugin<?> plugin) {
          if (plugin.requiresConfidentiality() && !this.protocol.getSocketConnection().isSSLEstablished()) {
              throw ExceptionFactory.createException(
                      Messages.getString("AuthenticationProvider.AuthenticationPluginRequiresSSL", new Object[] { plugin.getProtocolPluginName() }),
                      getExceptionInterceptor());
          }
      }
  
      /**
       * Performs an authentication handshake to authorize connection to a given database as a given MySQL user.
       * This can happen upon initial connection to the server, after receiving Auth Challenge Packet, or
       * at any moment during the connection life-time via a Change User request.
       * 
       * This method will use registered authentication plugins as requested by the server.
       * 
       * @param challenge
       *            the Auth Challenge Packet received from server if
       *            this method is used during the initial connection.
       *            Otherwise null.
       */
      private void proceedHandshakeWithPluggableAuthentication(final NativePacketPayload challenge) {
          ServerSession serverSession = this.protocol.getServerSession();
  
          if (this.authenticationPlugins == null) {
              loadAuthenticationPlugins();
          }
  
          boolean forChangeUser = true;
          if (challenge != null) {
              this.serverDefaultAuthenticationPluginName = challenge.readString(StringSelfDataType.STRING_TERM, "ASCII");
              forChangeUser = false;
          }
  
          serverSession.getCharsetSettings().configurePreHandshake(forChangeUser);
  
          /*
           * Select the initial plugin:
           * Choose the client-side default authentication plugin, if explicitely specified, otherwise choose the server-side default authentication plugin.
           */
          String pluginName;
          if (this.clientDefaultAuthenticationPluginExplicitelySet) {
              pluginName = this.clientDefaultAuthenticationPluginName;
          } else {
              pluginName = this.serverDefaultAuthenticationPluginName != null ? this.serverDefaultAuthenticationPluginName
                      : this.clientDefaultAuthenticationPluginName;
          }
          AuthenticationPlugin<NativePacketPayload> plugin = getAuthenticationPlugin(pluginName);
  
          if (plugin == null) {
              /* Use default if there is no plugin for pluginName. */
              pluginName = this.clientDefaultAuthenticationPluginName;
              plugin = getAuthenticationPlugin(pluginName);
          }
  
          boolean skipPassword = false;
          if (pluginName.equals(Sha256PasswordPlugin.PLUGIN_NAME) && !pluginName.equals(this.clientDefaultAuthenticationPluginName)
                  && !this.protocol.getSocketConnection().isSSLEstablished()
                  && this.propertySet.getStringProperty(PropertyKey.serverRSAPublicKeyFile).getValue() == null
                  && !this.propertySet.getBooleanProperty(PropertyKey.allowPublicKeyRetrieval).getValue()) {
              /*
               * Fall back to client default if plugin is 'sha256_password' but required conditions for this to work aren't met.
               * If client default is other than 'sha256_password', this will result in an immediate authentication switch request, allowing for other plugins to
               * authenticate successfully. If client default is 'sha256_password' then the authentication will fail as expected. In both cases user's password
               * won't be sent to avoid subjecting it to lesser security levels.
               */
              plugin = getAuthenticationPlugin(this.clientDefaultAuthenticationPluginName);
              skipPassword = true;
          }
  
          checkConfidentiality(plugin);
  
          // Servers not affected by Bug#70865 expect the Change User Request containing a correct answer to seed sent by the server during the initial handshake,
          // thus we reuse it here. Servers affected by Bug#70865 will just ignore it and send the Auth Switch.
          NativePacketPayload fromServer = new NativePacketPayload(StringUtils.getBytes(this.seed));
          String sourceOfAuthData = this.serverDefaultAuthenticationPluginName;
  
          NativePacketPayload lastSent = null;
          NativePacketPayload lastReceived = challenge;
          ArrayList<NativePacketPayload> toServer = new ArrayList<>();
  
          boolean firstPacket = true;
  
          // MFA authentication factor
          int mfaNthFactor = 1;
  
          /* Max iterations number */
          int counter = 100;
          while (0 < counter--) {
              /*
               * call plugin
               */
              plugin.setAuthenticationParameters(this.username, skipPassword ? null : getNthFactorPassword(mfaNthFactor));
              plugin.setSourceOfAuthData(sourceOfAuthData);
              plugin.nextAuthenticationStep(fromServer, toServer);
  
              /*
               * send response to server
               */
              if (firstPacket) {
                  NativePacketPayload authData = toServer.isEmpty() ? new NativePacketPayload(0) : toServer.get(0);
                  if (forChangeUser) {
                      // write COM_CHANGE_USER Packet
                      lastSent = createChangeUserPacket(serverSession, plugin.getProtocolPluginName(), authData);
                      this.protocol.send(lastSent, lastSent.getPosition());
                  } else {
                      // write HandshakeResponse packet
                      lastSent = createHandshakeResponsePacket(serverSession, plugin.getProtocolPluginName(), authData);
                      this.protocol.send(lastSent, lastSent.getPosition());
                  }
                  firstPacket = false;
              } else if (!toServer.isEmpty()) {
                  // write AuthSwitchResponse packet or raw packet(s)
                  toServer.forEach(b -> this.protocol.send(b, b.getPayloadLength()));
              }
  
              /*
               * read packet from server
               */
              lastReceived = this.protocol.checkErrorMessage();
  
              if (lastReceived.isOKPacket()) {
                  // read OK packet
                  OkPacket ok = OkPacket.parse(lastReceived, null);
                  serverSession.setStatusFlags(ok.getStatusFlags(), true);
                  serverSession.getServerSessionStateController().setSessionStateChanges(ok.getSessionStateChanges());
  
                  // authentication complete
                  plugin.destroy();
                  break;
  
              } else if (lastReceived.isAuthMethodSwitchRequestPacket()) {
                  // read AuthSwitchRequest Packet
                  skipPassword = false;
                  pluginName = lastReceived.readString(StringSelfDataType.STRING_TERM, "ASCII");
                  if (plugin.getProtocolPluginName().equals(pluginName)) {
                      plugin.reset(); // just reset the current one
                  } else {
                      // get new plugin
                      plugin.destroy();
                      plugin = getAuthenticationPlugin(pluginName);
                      if (plugin == null) {
                          throw ExceptionFactory.createException(WrongArgumentException.class,
                                  Messages.getString("AuthenticationProvider.BadAuthenticationPlugin", new Object[] { pluginName }), getExceptionInterceptor());
                      }
                  }
  
                  checkConfidentiality(plugin);
                  fromServer = new NativePacketPayload(lastReceived.readBytes(StringSelfDataType.STRING_EOF));
  
              } else if (lastReceived.isAuthNextFactorPacket()) {
                  // authentication not done yet, there's another MFA iteration 
                  mfaNthFactor++;
                  skipPassword = false;
                  pluginName = lastReceived.readString(StringSelfDataType.STRING_TERM, "ASCII");
                  if (plugin.getProtocolPluginName().equals(pluginName)) {
                      plugin.reset(); // just reset the current one
                  } else {
                      // get new plugin
                      plugin.destroy();
                      plugin = getAuthenticationPlugin(pluginName);
                      if (plugin == null) {
                          throw ExceptionFactory.createException(WrongArgumentException.class,
                                  Messages.getString("AuthenticationProvider.BadAuthenticationPlugin", new Object[] { pluginName }), getExceptionInterceptor());
                      }
                  }
  
                  checkConfidentiality(plugin);
                  fromServer = new NativePacketPayload(lastReceived.readBytes(StringSelfDataType.STRING_EOF));
  
              } else {
                  // read raw (from AuthMoreData) packet 
                  if (!this.protocol.versionMeetsMinimum(5, 5, 16)) {
                      lastReceived.setPosition(lastReceived.getPosition() - 1);
                  }
                  fromServer = new NativePacketPayload(lastReceived.readBytes(StringSelfDataType.STRING_EOF));
              }
  
              sourceOfAuthData = pluginName;
          }
  
          if (counter == 0) {
              throw ExceptionFactory.createException(WrongArgumentException.class,
                      Messages.getString("CommunicationsException.TooManyAuthenticationPluginNegotiations"), getExceptionInterceptor());
          }
  
          this.protocol.afterHandshake();
  
          if (!this.useConnectWithDb) {
              this.protocol.changeDatabase(this.database);
          }
      }
  
      private String getNthFactorPassword(int nthFactor) {
          switch (nthFactor) {
              case 1:
                  return this.password == null ? this.protocol.getPropertySet().getStringProperty(PropertyKey.password1).getValue() : this.password;
              case 2:
                  return this.protocol.getPropertySet().getStringProperty(PropertyKey.password2).getValue();
              case 3:
                  return this.protocol.getPropertySet().getStringProperty(PropertyKey.password3).getValue();
              default:
                  return null;
          }
      }
  
      private Map<String, String> getConnectionAttributesMap(String attStr) {
  
          Map<String, String> attMap = new HashMap<>();
  
          if (attStr != null) {
              String[] pairs = attStr.split(",");
              for (String pair : pairs) {
                  int keyEnd = pair.indexOf(":");
                  if (keyEnd > 0 && (keyEnd + 1) < pair.length()) {
                      attMap.put(pair.substring(0, keyEnd), pair.substring(keyEnd + 1));
                  }
              }
          }
  
          // Leaving disabled until standard values are defined
          // props.setProperty("_os", NonRegisteringDriver.OS);
          // props.setProperty("_platform", NonRegisteringDriver.PLATFORM);
          attMap.put("_client_name", Constants.CJ_NAME);
          attMap.put("_client_version", Constants.CJ_VERSION);
          attMap.put("_runtime_vendor", Constants.JVM_VENDOR);
          attMap.put("_runtime_version", Constants.JVM_VERSION);
          attMap.put("_client_license", Constants.CJ_LICENSE);
  
          return attMap;
      }
  
      private void appendConnectionAttributes(NativePacketPayload buf, String attributes, String enc) {
  
          NativePacketPayload lb = new NativePacketPayload(100);
          Map<String, String> attMap = getConnectionAttributesMap(attributes);
  
          for (String key : attMap.keySet()) {
              lb.writeBytes(StringSelfDataType.STRING_LENENC, StringUtils.getBytes(key, enc));
              lb.writeBytes(StringSelfDataType.STRING_LENENC, StringUtils.getBytes(attMap.get(key), enc));
          }
  
          buf.writeInteger(IntegerDataType.INT_LENENC, lb.getPosition());
          buf.writeBytes(StringLengthDataType.STRING_FIXED, lb.getByteBuffer(), 0, lb.getPosition());
      }
  
      public ExceptionInterceptor getExceptionInterceptor() {
          return this.exceptionInterceptor;
      }
  
      /**
       * Re-authenticates as the given user and password
       * 
       * @param user
       *            user name
       * @param pass
       *            password
       * @param db
       *            database name
       */
      @Override
      public void changeUser(String user, String pass, String db) {
          this.username = user;
          this.password = pass;
          this.database = db;
          proceedHandshakeWithPluggableAuthentication(null);
          this.password = null;
      }
  
      private NativePacketPayload createHandshakeResponsePacket(ServerSession serverSession, String pluginName, NativePacketPayload authData) {
  
          long clientParam = serverSession.getClientParam();
          int collationIndex = serverSession.getCharsetSettings().configurePreHandshake(false);
          String enc = serverSession.getCharsetSettings().getPasswordCharacterEncoding();
  
          int userLength = this.username == null ? 0 : this.username.length();
          NativePacketPayload last_sent = new NativePacketPayload(AUTH_411_OVERHEAD + 7 //
                  + 48                                                                  // passwordLength
                  + (3 * userLength)                                                    // userLength
                  + (this.useConnectWithDb ? 3 * this.database.length() : 0)            // databaseLength
          );
          last_sent.writeInteger(IntegerDataType.INT4, clientParam);
          last_sent.writeInteger(IntegerDataType.INT4, NativeConstants.MAX_PACKET_SIZE);
          last_sent.writeInteger(IntegerDataType.INT1, collationIndex);
          last_sent.writeBytes(StringLengthDataType.STRING_FIXED, new byte[23]);   // Set of bytes reserved for future use.
  
          // User/Password data
          last_sent.writeBytes(StringSelfDataType.STRING_TERM, StringUtils.getBytes(this.username, enc));
          if ((clientParam & NativeServerSession.CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA) != 0) {
              // send lenenc-int length of auth-response and string[n] auth-response
              last_sent.writeBytes(StringSelfDataType.STRING_LENENC, authData.readBytes(StringSelfDataType.STRING_EOF));
          } else {
              // send 1 byte length of auth-response and string[n] auth-response
              last_sent.writeInteger(IntegerDataType.INT1, authData.getPayloadLength());
              last_sent.writeBytes(StringSelfDataType.STRING_EOF, authData.getByteBuffer());
          }
  
          if (this.useConnectWithDb) {
              last_sent.writeBytes(StringSelfDataType.STRING_TERM, StringUtils.getBytes(this.database, enc));
          }
  
          last_sent.writeBytes(StringSelfDataType.STRING_TERM, StringUtils.getBytes(pluginName, enc));
  
          // connection attributes
          if (((clientParam & NativeServerSession.CLIENT_CONNECT_ATTRS) != 0)) {
              appendConnectionAttributes(last_sent, this.propertySet.getStringProperty(PropertyKey.connectionAttributes).getValue(), enc);
          }
          return last_sent;
      }
  
      private NativePacketPayload createChangeUserPacket(ServerSession serverSession, String pluginName, NativePacketPayload authData) {
          // write Auth Response Packet
          long clientParam = serverSession.getClientParam();
          int collationIndex = serverSession.getCharsetSettings().configurePreHandshake(false);
          String enc = serverSession.getCharsetSettings().getPasswordCharacterEncoding();
  
          NativePacketPayload last_sent = new NativePacketPayload(AUTH_411_OVERHEAD + 7 //
                  + 48                // passwordLength
                  + 3 * this.username.length() // userLength
                  + (this.useConnectWithDb ? 3 * this.database.length() : 1)   // databaseLength
                  + 1);
          last_sent.writeInteger(IntegerDataType.INT1, NativeConstants.COM_CHANGE_USER);
  
          // User/Password data
          last_sent.writeBytes(StringSelfDataType.STRING_TERM, StringUtils.getBytes(this.username, enc));
          // 'auth-response-len' is limited to one Byte but, in case of success, COM_CHANGE_USER will be followed by an AuthSwitchRequest anyway
          if (authData.getPayloadLength() < 256) {
              // non-mysql servers may use this information to authenticate without requiring another round-trip
              last_sent.writeInteger(IntegerDataType.INT1, authData.getPayloadLength());
              last_sent.writeBytes(StringSelfDataType.STRING_EOF, authData.getByteBuffer(), 0, authData.getPayloadLength());
          } else {
              last_sent.writeInteger(IntegerDataType.INT1, 0);
          }
  
          if (this.useConnectWithDb) {
              last_sent.writeBytes(StringSelfDataType.STRING_TERM, StringUtils.getBytes(this.database, enc));
          } else {
              /* For empty database */
              last_sent.writeInteger(IntegerDataType.INT1, 0);
          }
  
          last_sent.writeInteger(IntegerDataType.INT1, collationIndex);
          last_sent.writeInteger(IntegerDataType.INT1, 0); // two (little-endian) bytes for charset in this packet
  
          // plugin name
          last_sent.writeBytes(StringSelfDataType.STRING_TERM, StringUtils.getBytes(pluginName, enc));
  
          // connection attributes
          if ((clientParam & NativeServerSession.CLIENT_CONNECT_ATTRS) != 0) {
              appendConnectionAttributes(last_sent, this.propertySet.getStringProperty(PropertyKey.connectionAttributes).getValue(), enc);
          }
          return last_sent;
      }
  }
  