Repository: microsoft/mssql-jdbc
Pull request #1780 (2022-03-29 01:04:08)

Add encrypt utility to obfuscate password strings
This allows storing of the trustStorePassword in encrypted form since it is needed for Idle Connection Resiliency when recovering connections. 


Patch:
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java
index cd1bed31..f44d36c3 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java
@@ -18,6 +18,8 @@ import java.io.UnsupportedEncodingException;
 import java.math.BigDecimal;
 import java.math.BigInteger;
 import java.math.RoundingMode;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
 import java.net.Socket;
@@ -745,17 +747,21 @@ final class TDSChannel implements Serializable {
 
     /**
      * Opens the physical communications channel (TCP/IP socket and I/O streams) to the SQL Server.
+     * 
+     * @param iPAddressPreference
+     *        Preferred type of IP address to use first
      *
      * @return InetSocketAddress of the connection socket.
      */
     final InetSocketAddress open(String host, int port, int timeoutMillis, boolean useParallel, boolean useTnir,
-            boolean isTnirFirstAttempt, int timeoutMillisForFullTimeout) throws SQLServerException {
+            boolean isTnirFirstAttempt, int timeoutMillisForFullTimeout,
+            String iPAddressPreference) throws SQLServerException {
         if (logger.isLoggable(Level.FINER))
             logger.finer(this.toString() + ": Opening TCP socket...");
 
         SocketFinder socketFinder = new SocketFinder(traceID, con);
         channelSocket = tcpSocket = socketFinder.findSocket(host, port, timeoutMillis, useParallel, useTnir,
-                isTnirFirstAttempt, timeoutMillisForFullTimeout);
+                isTnirFirstAttempt, timeoutMillisForFullTimeout, iPAddressPreference);
         try {
             // Set socket options
             tcpSocket.setTcpNoDelay(true);
@@ -1576,8 +1582,12 @@ final class TDSChannel implements Serializable {
 
             String trustStoreFileName = con.activeConnectionProperties
                     .getProperty(SQLServerDriverStringProperty.TRUST_STORE.toString());
-            String trustStorePassword = con.activeConnectionProperties
-                    .getProperty(SQLServerDriverStringProperty.TRUST_STORE_PASSWORD.toString());
+
+            String trustStorePassword = null;
+            if (con.encryptedTrustStorePassword != null) {
+                trustStorePassword = SecureStringUtil.getInstance().getDecryptedString(con.encryptedTrustStorePassword);
+            }
+
             String hostNameInCertificate = con.activeConnectionProperties
                     .getProperty(SQLServerDriverStringProperty.HOSTNAME_IN_CERTIFICATE.toString());
 
@@ -2323,6 +2333,9 @@ final class SocketFinder {
     // necessary for raising exceptions so that the connection pool can be notified
     private final SQLServerConnection conn;
 
+    // list of addresses for ip selection by type preference
+    private static ArrayList<InetAddress> addressList = new ArrayList<>();
+
     /**
      * Constructs a new SocketFinder object with appropriate traceId
      * 
@@ -2342,11 +2355,13 @@ final class SocketFinder {
      * @param hostName
      * @param portNumber
      * @param timeoutInMilliSeconds
+     * @param iPAddressPreference
      * @return connected socket
      * @throws IOException
      */
     Socket findSocket(String hostName, int portNumber, int timeoutInMilliSeconds, boolean useParallel, boolean useTnir,
-            boolean isTnirFirstAttempt, int timeoutInMilliSecondsForFullTimeout) throws SQLServerException {
+            boolean isTnirFirstAttempt, int timeoutInMilliSecondsForFullTimeout,
+            String iPAddressPreference) throws SQLServerException {
         assert timeoutInMilliSeconds != 0 : "The driver does not allow a time out of 0";
 
         try {
@@ -2356,9 +2371,10 @@ final class SocketFinder {
                 // MSF is false. TNIR could be true or false. DBMirroring could be true or false.
                 // For TNIR first attempt, we should do existing behavior including how host name is resolved.
                 if (useTnir && isTnirFirstAttempt) {
-                    return getDefaultSocket(hostName, portNumber, SQLServerConnection.TnirFirstAttemptTimeoutMs);
+                    return getSocketByIPPreference(hostName, portNumber, SQLServerConnection.TnirFirstAttemptTimeoutMs,
+                            iPAddressPreference);
                 } else if (!useTnir) {
-                    return getDefaultSocket(hostName, portNumber, timeoutInMilliSeconds);
+                    return getSocketByIPPreference(hostName, portNumber, timeoutInMilliSeconds, iPAddressPreference);
                 }
             }
 
@@ -2651,19 +2667,85 @@ final class SocketFinder {
         return socketFactory;
     }
 
-    // This method contains the old logic of connecting to
-    // a socket of one of the IPs corresponding to a given host name.
-    // In the old code below, the logic around 0 timeout has been removed as
-    // 0 timeout is not allowed. The code has been re-factored so that the logic
-    // is common for hostName or InetAddress.
-    private Socket getDefaultSocket(String hostName, int portNumber, int timeoutInMilliSeconds) throws IOException {
-        // Open the socket, with or without a timeout, throwing an UnknownHostException
-        // if there is a failure to resolve the host name to an InetSocketAddress.
-        //
+    /**
+     * Helper function which traverses through list of InetAddresses to find a resolved one
+     * @param hostName 
+     * 
+     * @param portNumber
+     *        Port Number
+     * @return First resolved address or unresolved address if none found
+     * @throws IOException
+     * @throws SQLServerException
+     */
+    private InetSocketAddress getInetAddressByIPPreference(String hostName, int portNumber) throws IOException, SQLServerException {
+        InetSocketAddress addr = InetSocketAddress.createUnresolved(hostName, portNumber);
+        for (int i = 0; i < addressList.size(); i++) {
+            addr = new InetSocketAddress(addressList.get(i), portNumber);
+            if (!addr.isUnresolved())
+                return addr;
+        }
+        return addr;
+    }
+
+    /**
+     * This method makes a connected socket given a hostname or IP address.
+     * 
+     * @param hostName
+     *        Hostname or IP Address
+     * @param portNumber
+     *        Port number
+     * @param timeoutInMilliSeconds
+     *        Timeout
+     * @param iPAddressPreference
+     *        Preferred IP address type
+     * @return Connected Socket
+     * @throws IOException
+     * @throws SQLServerException
+     */
+    private Socket getSocketByIPPreference(String hostName, int portNumber, int timeoutInMilliSeconds,
+            String iPAddressPreference) throws IOException, SQLServerException {
+        InetSocketAddress addr = null;
+        InetAddress addresses[] = InetAddress.getAllByName(hostName);
+        IPAddressPreference pref = IPAddressPreference.valueOfString(iPAddressPreference);
+        switch (pref) {
+            case IPv6First:
+                // Try to connect to first choice of IP address type
+                fillAddressList(addresses, true);
+                addr = getInetAddressByIPPreference(hostName, portNumber);
+                if (!addr.isUnresolved())
+                    return getConnectedSocket(addr, timeoutInMilliSeconds);
+                // No unresolved addresses of preferred type, try the other
+                fillAddressList(addresses, false);
+                addr = getInetAddressByIPPreference(hostName, portNumber);
+                if (!addr.isUnresolved())
+                    return getConnectedSocket(addr, timeoutInMilliSeconds);
+                break;
+            case IPv4First:
+                // Try to connect to first choice of IP address type
+                fillAddressList(addresses, false);
+                addr = getInetAddressByIPPreference(hostName, portNumber);
+                if (!addr.isUnresolved())
+                    return getConnectedSocket(addr, timeoutInMilliSeconds);
+                // No unresolved addresses of preferred type, try the other
+                fillAddressList(addresses, true);
+                addr = getInetAddressByIPPreference(hostName, portNumber);
+                if (!addr.isUnresolved())
+                    return getConnectedSocket(addr, timeoutInMilliSeconds);
+                break;
+            case UsePlatformDefault:
+                for (InetAddress address : addresses) {
+                    addr = new InetSocketAddress(address, portNumber);
+                    if (!addr.isUnresolved())
+                        return getConnectedSocket(addr, timeoutInMilliSeconds);
+                }
+                break;
+            default:
+                break;
+        }
+
         // Note that Socket(host, port) throws an UnknownHostException if the host name
         // cannot be resolved, but that InetSocketAddress(host, port) does not - it sets
         // the returned InetSocketAddress as unresolved.
-        InetSocketAddress addr = new InetSocketAddress(hostName, portNumber);
         if (addr.isUnresolved()) {
             if (logger.isLoggable(Level.FINER)) {
                 logger.finer(this.toString() + "Failed to resolve host name: " + hostName
@@ -2672,9 +2754,32 @@ final class SocketFinder {
             InetSocketAddress cacheEntry = SQLServerConnection.getDNSEntry(hostName);
             addr = (null != cacheEntry) ? cacheEntry : addr;
         }
+
         return getConnectedSocket(addr, timeoutInMilliSeconds);
     }
 
+    /**
+     * Fills static array of IP Addresses with addresses of the preferred protocol version.
+     * @param addresses Array of all addresses
+     * @param ipv6first Boolean switch for IPv6 first
+     */
+    private void fillAddressList(InetAddress[] addresses, boolean ipv6first) {
+        addressList.clear();
+        if (ipv6first) {
+            for (InetAddress addr : addresses) {
+                if (addr instanceof Inet6Address) {
+                    addressList.add(addr);
+                }
+            }
+        } else {
+            for (InetAddress addr : addresses) {
+                if (addr instanceof Inet4Address) {
+                    addressList.add(addr);
+                }
+            }
+        }
+    }
+
     private Socket getConnectedSocket(InetAddress inetAddr, int portNumber,
             int timeoutInMilliSeconds) throws IOException {
         InetSocketAddress addr = new InetSocketAddress(inetAddr, portNumber);
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection.java
index a74a3bd1..a224eb7f 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection.java
@@ -391,4 +391,20 @@ public interface ISQLServerConnection extends java.sql.Connection {
      *        boolean value for 'delayLoadingLobs'.
      */
     void setDelayLoadingLobs(boolean delayLoadingLobs);
+    
+    /**
+     * Sets the name of the preferred type of IP Address.
+     * 
+     * @param iPAddressPreference
+     *        A String that contains the preferred type of IP Address.
+     */
+    void setIPAddressPreference(String iPAddressPreference);
+    
+    /**
+     * Gets the name of the preferred type of IP Address.
+     * 
+     * @return IPAddressPreference
+     *        A String that contains the preferred type of IP Address.
+     */
+    String getIPAddressPreference();
 }
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java
index a6d27b80..238c4e51 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java
@@ -387,6 +387,22 @@ public interface ISQLServerDataSource extends javax.sql.CommonDataSource {
      * @return A String that contains the server name or null if no value is set.
      */
     String getServerName();
+    
+    /**
+     * Sets the name of the preferred type of IP Address.
+     * 
+     * @param iPAddressPreference
+     *        A String that contains the preferred type of IP Address.
+     */
+    void setIPAddressPreference(String iPAddressPreference);
+    
+    /**
+     * Gets the name of the preferred type of IP Address.
+     * 
+     * @return IPAddressPreference
+     *        A String that contains the preferred type of IP Address.
+     */
+    String getIPAddressPreference();
 
     /**
      * Sets the name of the failover server that is used in a database mirroring configuration.
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java
index c025ef72..f8f0edda 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java
@@ -217,6 +217,8 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
     /** flag indicating whether prelogin TLS handshake is required */
     private boolean isTDSS = false;
 
+    String encryptedTrustStorePassword = null;
+
     /**
      * Return an existing cached SharedTimer associated with this Connection or create a new one.
      *
@@ -391,7 +393,7 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
         private Instant lastNetworkActivity = Instant.now();
 
         /**
-         * An “idle” connection will only ever get its socket disconnected by a keepalive packet after a connection has
+         * An "idle" connection will only ever get its socket disconnected by a keepalive packet after a connection has
          * been severed. KeepAlive packets are only sent on idle sockets. Default setting by the driver (on platforms
          * that have Java support for setting it) and the recommended setting is 30s (and OS default for those that
          * don't set it is 2 hrs).
@@ -1826,6 +1828,13 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
 
                 pooledConnectionParent = pooledConnection;
 
+                String trustStorePassword = activeConnectionProperties
+                        .getProperty(SQLServerDriverStringProperty.TRUST_STORE_PASSWORD.toString());
+                if (trustStorePassword != null) {
+                    encryptedTrustStorePassword = SecureStringUtil.getInstance().getEncryptedString(trustStorePassword);
+                    activeConnectionProperties.remove(SQLServerDriverStringProperty.TRUST_STORE_PASSWORD.toString());
+                }
+
                 String hostNameInCertificate = activeConnectionProperties
                         .getProperty(SQLServerDriverStringProperty.HOSTNAME_IN_CERTIFICATE.toString());
 
@@ -1937,6 +1946,15 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
                 if (null != sPropValuePort) {
                     trustedServerNameAE += ":" + sPropValuePort;
                 }
+                
+                sPropKey = SQLServerDriverStringProperty.IPADDRESS_PREFERENCE.toString();
+                sPropValue = activeConnectionProperties.getProperty(sPropKey);
+                if (null == sPropValue) {
+                    sPropValue = SQLServerDriverStringProperty.IPADDRESS_PREFERENCE.getDefaultValue();
+                    activeConnectionProperties.setProperty(sPropKey, sPropValue);
+                } else {
+                    activeConnectionProperties.setProperty(sPropKey, IPAddressPreference.valueOfString(sPropValue).toString());
+                }
 
                 sPropKey = SQLServerDriverStringProperty.APPLICATION_NAME.toString();
                 sPropValue = activeConnectionProperties.getProperty(sPropKey);
@@ -3229,10 +3247,11 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
 
         // if the timeout is infinite slices are infinite too.
         tdsChannel = new TDSChannel(this);
+        String iPAddressPreference = activeConnectionProperties.getProperty(SQLServerDriverStringProperty.IPADDRESS_PREFERENCE.toString());
 
         InetSocketAddress inetSocketAddress = tdsChannel.open(serverInfo.getParsedServerName(),
                 serverInfo.getPortNumber(), (0 == timeOutFullInSeconds) ? 0 : timeOutSliceInMillis, useParallel,
-                useTnir, isTnirFirstAttempt, timeOutsliceInMillisForFullTimeout);
+                useTnir, isTnirFirstAttempt, timeOutsliceInMillisForFullTimeout, iPAddressPreference);
 
         setState(State.Connected);
 
@@ -7624,6 +7643,17 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
     String getServerName() {
         return this.trustedServerNameAE;
     }
+
+    @Override
+    public void setIPAddressPreference(String iPAddressPreference) {
+        activeConnectionProperties.setProperty(SQLServerDriverStringProperty.IPADDRESS_PREFERENCE.toString(), iPAddressPreference);
+        
+    }
+
+    @Override
+    public String getIPAddressPreference() {
+        return activeConnectionProperties.getProperty(SQLServerDriverStringProperty.IPADDRESS_PREFERENCE.toString());
+    }
 }
 
 
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionPoolProxy.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionPoolProxy.java
index 91678f39..364f549d 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionPoolProxy.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionPoolProxy.java
@@ -619,4 +619,15 @@ class SQLServerConnectionPoolProxy implements ISQLServerConnection, java.io.Seri
     public void setDelayLoadingLobs(boolean delayLoadingLobs) {
         wrappedConnection.setDelayLoadingLobs(delayLoadingLobs);
     }
+
+    @Override
+    public void setIPAddressPreference(String iPAddressPreference) {
+        wrappedConnection.setIPAddressPreference(iPAddressPreference);
+        
+    }
+
+    @Override
+    public String getIPAddressPreference() {
+        return wrappedConnection.getIPAddressPreference();
+    }
 }
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java
index f7ea7668..c30d6b15 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java
@@ -594,6 +594,24 @@ public class SQLServerDataSource
     public String getServerName() {
         return getStringProperty(connectionProps, SQLServerDriverStringProperty.SERVER_NAME.toString(), null);
     }
+    
+    /**
+     * Set the preferred type of IP Address
+     * 
+     * @param iPAddressPreference Preferred IP Address type
+     */
+    @Override
+    public void setIPAddressPreference(String iPAddressPreference) {
+        setStringProperty(connectionProps, SQLServerDriverStringProperty.IPADDRESS_PREFERENCE.toString(), iPAddressPreference);
+    }
+    
+    /**
+     * Gets the preferred type of IP Address
+     */
+    @Override
+    public String getIPAddressPreference() {
+        return getStringProperty(connectionProps, SQLServerDriverStringProperty.IPADDRESS_PREFERENCE.toString(), SQLServerDriverStringProperty.IPADDRESS_PREFERENCE.getDefaultValue());
+    }
 
     /**
      * Sets the realm for Kerberos authentication.
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java
index 67b7e604..43209c8f 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java
@@ -257,6 +257,43 @@ enum SSLProtocol {
 }
 
 
+enum IPAddressPreference {
+    IPv4First("IPv4First"),
+    IPv6First("IPv6First"),
+    UsePlatformDefault("UsePlatformDefault");
+
+    private final String name;
+
+    IPAddressPreference(String name) {
+        this.name = name;
+    }
+
+    @Override
+    public String toString() {
+        return name;
+    }
+
+    static IPAddressPreference valueOfString(String value) throws SQLServerException {
+        IPAddressPreference iptype = null;
+
+        if (value.toLowerCase(Locale.US).equalsIgnoreCase(IPAddressPreference.IPv4First.toString())) {
+            iptype = IPAddressPreference.IPv4First;
+        } else if (value.toLowerCase(Locale.US).equalsIgnoreCase(IPAddressPreference.IPv6First.toString())) {
+            iptype = IPAddressPreference.IPv6First;
+        } else if (value.toLowerCase(Locale.US)
+                .equalsIgnoreCase(IPAddressPreference.UsePlatformDefault.toString())) {
+            iptype = IPAddressPreference.UsePlatformDefault;
+
+        } else {
+            MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_InvalidIPAddressPreference"));
+            Object[] msgArgs = {value};
+            throw new SQLServerException(form.format(msgArgs), null);
+        }
+        return iptype;
+    }
+}
+
+
 enum KeyStoreAuthentication {
     JavaKeyStorePassword,
     KeyVaultClientSecret,
@@ -422,6 +459,7 @@ enum SQLServerDriverStringProperty {
     SELECT_METHOD("selectMethod", "direct"),
     DOMAIN("domain", ""),
     SERVER_NAME("serverName", ""),
+    IPADDRESS_PREFERENCE("iPAddressPreference", IPAddressPreference.IPv4First.toString()),
     SERVER_SPN("serverSpn", ""),
     REALM("realm", ""),
     SOCKET_FACTORY_CLASS("socketFactoryClass", ""),
@@ -652,6 +690,11 @@ public final class SQLServerDriver implements java.sql.Driver {
                     SQLServerDriverStringProperty.DOMAIN.getDefaultValue(), false, null),
             new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.SERVER_NAME.toString(),
                     SQLServerDriverStringProperty.SERVER_NAME.getDefaultValue(), false, null),
+            new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.IPADDRESS_PREFERENCE.toString(),
+                    SQLServerDriverStringProperty.IPADDRESS_PREFERENCE.getDefaultValue(), false,
+                    new String[] {IPAddressPreference.IPv4First.toString(),
+                            IPAddressPreference.IPv6First.toString(),
+                            IPAddressPreference.UsePlatformDefault.toString()}),
             new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.SERVER_SPN.toString(),
                     SQLServerDriverStringProperty.SERVER_SPN.getDefaultValue(), false, null),
             new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.REALM.toString(),
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java
index f1186553..bb98b34d 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java
@@ -165,6 +165,7 @@ public final class SQLServerResource extends ListResourceBundle {
         {"R_databaseNamePropertyDescription", "The name of the database to connect to."},
         {"R_domainPropertyDescription", "The Windows domain to authenticate in using NTLM."},
         {"R_serverNamePropertyDescription", "The computer running SQL Server."},
+        {"R_iPAddressPreferencePropertyDescription", "The preferred type of IP address."},
         {"R_portNumberPropertyDescription", "The TCP port where an instance of SQL Server is listening."},
         {"R_realmPropertyDescription", "The realm for Kerberos authentication."},
         {"R_serverSpnPropertyDescription", "SQL Server SPN."},
@@ -267,8 +268,8 @@ public final class SQLServerResource extends ListResourceBundle {
         {"R_InvalidCipherTextSize", "Specified ciphertext has an invalid size of {0} bytes, which is below the minimum {1} bytes required for decryption."},
         {"R_InvalidAlgorithmVersion", "The specified ciphertext''s encryption algorithm version {0} does not match the expected encryption algorithm version {1} ."},
         {"R_InvalidAuthenticationTag", "Specified ciphertext has an invalid authentication tag. "},
-        {"R_EncryptionFailed", "Internal error while encryption:  {0} "},
-        {"R_DecryptionFailed", "Internal error while decryption:  {0} "},
+        {"R_EncryptionFailed", "Internal error during encryption:  {0} "},
+        {"R_DecryptionFailed", "Internal error during decryption:  {0} "},
         {"R_InvalidKeySize", "The column encryption key has been successfully decrypted but it''s length: {0} does not match the length: {1} for algorithm \"{2}\". Verify the encrypted value of the column encryption key in the database."},
         {"R_InvalidEncryptionType", "Encryption type {0} specified for the column in the database is either invalid or corrupted. Valid encryption types for algorithm {1} are: {2}."},
         {"R_UnknownColumnEncryptionAlgorithm", "The Algorithm {0} does not exist. Algorithms registered in the factory are {1}."},
@@ -502,9 +503,11 @@ public final class SQLServerResource extends ListResourceBundle {
         {"R_crClientUnrecoverable","The connection is broken and recovery is not possible. The connection is marked by the client driver as unrecoverable. No attempt was made to restore the connection."},
         {"R_crClientSSLStateNotRecoverable", "The server did not preserve SSL encryption during a recovery attempt, connection recovery is not possible."},
         {"R_crCommandCannotTimeOut", "Request failed to time out and SQLServerConnection does not exist"},
+        {"R_InvalidIPAddressPreference", "IP address preference {0} is not valid."},
         {"R_UnableLoadAuthDll", "Unable to load authentication DLL {0}"},
         {"R_illegalArgumentTrustManager", "Interal error. Peer certificate chain or key exchange algorithem can not be null or empty."},
         {"R_serverCertError", "Error validating Server Certificate: {0}."},
+        {"R_SecureStringInitFailed", "Failed to initialize SecureStringUtil to store secure strings"},
     };
 }
 // @formatter:on
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SecureStringUtil.java b/src/main/java/com/microsoft/sqlserver/jdbc/SecureStringUtil.java
new file mode 100644
index 00000000..c34979d1
--- /dev/null
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SecureStringUtil.java
@@ -0,0 +1,139 @@
+package com.microsoft.sqlserver.jdbc;
+
+import java.security.SecureRandom;
+import java.text.MessageFormat;
+import java.util.Base64;
+
+import javax.crypto.Cipher;
+import javax.crypto.KeyGenerator;
+import javax.crypto.spec.GCMParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+
+/**
+ * 
+ * This is an utility class to encrypt/decrypt strings. This is used to obfuscate passwords so they won't be visible as
+ * plaintext.
+ */
+final class SecureStringUtil {
+    /* cipher transformation in the form of algorithm/mode/padding */
+    static final String CIPHER_TRANSFORMATION = "AES/GCM/NoPadding";
+
+    /* key generator algorithm */
+    static final String KEYGEN_ALGORITHEM = "AES";
+
+    /* length of initialization vector buffer */
+    static final int IV_LENGTH = 12;
+
+    /* key size */
+    static final int KEY_SIZE = 256;
+
+    /* authentication tag length in bits */
+    static final int TAG_LENGTH = 16;
+
+    /* initialization vector */
+    byte[] iv;
+
+    /** secret key for encryption/decryption */
+    SecretKeySpec secretKey;
+
+    /* cryptographic cipher for encryption */
+    private Cipher encryptCipher;
+
+    /* cryptographic cipher for decryption */
+    private Cipher decryptCipher;
+
+    /* singleton instance */
+    private static SecureStringUtil instance;
+
+    /**
+     * Get reference to SecureStringUtil instance
+     * 
+     * @return the SecureStringUtil instance
+     * 
+     * @throws SQLServerException
+     *         if error
+     */
+    static SecureStringUtil getInstance() throws SQLServerException {
+        if (instance == null) {
+            instance = new SecureStringUtil();
+        }
+        return instance;
+    }
+
+    /**
+     * Creates an instance of the SecureStringUtil object and initialize values to encrypt/decrypt strings
+     * 
+     * @throws SQLServerException
+     *         if error
+     */
+    private SecureStringUtil() throws SQLServerException {
+        iv = new byte[IV_LENGTH];
+        try {
+            // generate key */
+            KeyGenerator keygen = KeyGenerator.getInstance(KEYGEN_ALGORITHEM);
+            keygen.init(KEY_SIZE);
+            secretKey = new SecretKeySpec(keygen.generateKey().getEncoded(), "AES");
+
+            // get ciphers for encryption/decryption
+            encryptCipher = Cipher.getInstance(CIPHER_TRANSFORMATION);
+            decryptCipher = Cipher.getInstance(CIPHER_TRANSFORMATION);
+        } catch (Exception e) {
+            MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_SecureStringInitFailed"));
+            Object[] msgArgs = {e.getMessage()};
+            throw new SQLServerException(this, form.format(msgArgs), null, 0, false);
+        }
+    }
+
+    /**
+     * Get encrypted value of given string
+     * 
+     * @param str
+     *        string to encrypt
+     * 
+     * @return encrypted string
+     * 
+     * @throws SQLServerException
+     *         if error
+     */
+    String getEncryptedString(String str) throws SQLServerException {
+        SecureRandom random = new SecureRandom();
+        random.nextBytes(iv);
+        GCMParameterSpec ivParamSpec = new GCMParameterSpec(TAG_LENGTH * 8, iv);
+
+        try {
+            encryptCipher.init(Cipher.ENCRYPT_MODE, secretKey, ivParamSpec);
+
+            byte[] cipherText = encryptCipher.doFinal(str.getBytes());
+            return Base64.getEncoder().encodeToString(cipherText);
+        } catch (Exception e) {
+            MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_EncryptionFailed"));
+            Object[] msgArgs = {e.getMessage()};
+            throw new SQLServerException(this, form.format(msgArgs), null, 0, false);
+        }
+    }
+
+    /**
+     * Get decrypted value of an encrypted string
+     * 
+     * @param str
+     * 
+     * @return decrypted string
+     * 
+     * @throws SQLServerException
+     */
+    String getDecryptedString(String str) throws SQLServerException {
+        GCMParameterSpec ivParamSpec = new GCMParameterSpec(TAG_LENGTH * 8, iv);
+
+        try {
+            decryptCipher.init(Cipher.DECRYPT_MODE, secretKey, ivParamSpec);
+
+            byte[] plainText = decryptCipher.doFinal(Base64.getDecoder().decode(str));
+            return new String(plainText);
+        } catch (Exception e) {
+            MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_DecryptionFailed"));
+            Object[] msgArgs = {e.getMessage()};
+            throw new SQLServerException(this, form.format(msgArgs), null, 0, false);
+        }
+    }
+}


Test patch:
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionTest.java
index d94100d4..f4139075 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionTest.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionTest.java
@@ -82,6 +82,9 @@ public class SQLServerConnectionTest extends AbstractTest {
 
         ds.setPortNumber(intPropValue);
         assertEquals(intPropValue, ds.getPortNumber(), TestResource.getResource("R_valuesAreDifferent"));
+        
+        ds.setIPAddressPreference(stringPropValue);
+        assertEquals(stringPropValue, ds.getIPAddressPreference(), TestResource.getResource("R_valuesAreDifferent"));
 
         ds.setURL(stringPropValue);
         assertEquals(stringPropValue, ds.getURL(), TestResource.getResource("R_valuesAreDifferent"));
@@ -658,7 +661,7 @@ public class SQLServerConnectionTest extends AbstractTest {
                 assertTrue(timeDiff <= milsecs, form.format(msgArgs));
             }
         } catch (Exception e) {
-            assertTrue(e.getMessage().contains(TestResource.getResource("R_cannotOpenDatabase")));
+            assertTrue(e.getMessage().contains(TestResource.getResource("R_cannotOpenDatabase")), e.getMessage());
             timerEnd = System.currentTimeMillis();
         }
     }
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/connection/ConnectionTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/connection/ConnectionTest.java
index 40110c6a..0f28b61f 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/connection/ConnectionTest.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/connection/ConnectionTest.java
@@ -12,10 +12,14 @@ import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Test;
 import org.junit.platform.runner.JUnitPlatform;
 import org.junit.runner.RunWith;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
 
 import com.microsoft.sqlserver.jdbc.SQLServerDataSource;
+import com.microsoft.sqlserver.jdbc.TestResource;
 import com.microsoft.sqlserver.testframework.AbstractTest;
 
+
 /*
  * This test is for testing various connection options
  */
@@ -36,10 +40,33 @@ public class ConnectionTest extends AbstractTest {
         ds.setKeyStoreSecret("placeholder");
 
         // Multiple, successive connections should not fail
-        try (Connection con = ds.getConnection()) {
-        }
+        try (Connection con = ds.getConnection()) {}
+
+        try (Connection con = ds.getConnection()) {}
+    }
+
+    @Test
+    public void testConnectWithIPAddressPreference() throws SQLException {
+        SQLServerDataSource ds = new SQLServerDataSource();
+        ds.setURL(connectionString);
+        ds.setIPAddressPreference("IPv4First");
+        try (Connection con = ds.getConnection()) {}
+        ds.setIPAddressPreference("IPv6First");
+        try (Connection con = ds.getConnection()) {}
+        ds.setIPAddressPreference("UsePlatformDefault");
+        try (Connection con = ds.getConnection()) {}
+    }
 
+    @Test
+    public void testInvalidConnectWithIPAddressPreference() throws SQLException {
+        SQLServerDataSource ds = new SQLServerDataSource();
+        ds.setURL(connectionString);
+        ds.setIPAddressPreference("Bogus");
         try (Connection con = ds.getConnection()) {
+            fail(TestResource.getResource("R_expectedFailPassed"));
+        } catch (Exception e) {
+            assertTrue(e.getMessage().matches(TestUtils.formatErrorMsg("R_InvalidIPAddressPreference")));
         }
     }
+
 }
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/connection/RequestBoundaryMethodsTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/connection/RequestBoundaryMethodsTest.java
index ee768ccf..5ab5428c 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/connection/RequestBoundaryMethodsTest.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/connection/RequestBoundaryMethodsTest.java
@@ -497,6 +497,8 @@ public class RequestBoundaryMethodsTest extends AbstractTest {
         verifiedMethodNames.add("registerColumnEncryptionKeyStoreProvidersOnConnection");
         verifiedMethodNames.add("getPrepareMethod");
         verifiedMethodNames.add("setPrepareMethod");
+        verifiedMethodNames.add("getIPAddressPreference");
+        verifiedMethodNames.add("setIPAddressPreference");
 
         return verifiedMethodNames;
     }