Repository: microsoft/mssql-jdbc
Pull request #1815 (2022-04-25 21:47:11)

Updated ssl cert validation test
Fixed the reflection used in the ssl cert validation test. This should fix the gradle pipeline.


Patch:
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java
index a924b39d..5dfa31e9 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java
@@ -1584,11 +1584,6 @@ final class TDSChannel implements Serializable {
             String trustStoreFileName = con.activeConnectionProperties
                     .getProperty(SQLServerDriverStringProperty.TRUST_STORE.toString());
 
-            String trustStorePassword = null;
-            if (con.encryptedTrustStorePassword != null) {
-                trustStorePassword = SecureStringUtil.getInstance().getDecryptedString(con.encryptedTrustStorePassword);
-            }
-
             String hostNameInCertificate = con.activeConnectionProperties
                     .getProperty(SQLServerDriverStringProperty.HOSTNAME_IN_CERTIFICATE.toString());
 
@@ -1656,7 +1651,7 @@ final class TDSChannel implements Serializable {
                     // If we are using the system default trustStore and trustStorePassword
                     // then we can skip all of the KeyStore loading logic below.
                     // The security provider's implementation takes care of everything for us.
-                    if (null == trustStoreFileName && null == trustStorePassword && !isTDSS) {
+                    if (null == trustStoreFileName && null == con.encryptedTrustStorePassword && !isTDSS) {
                         if (logger.isLoggable(Level.FINER)) {
                             logger.finer(toString() + " Using system default trust store and password");
                         }
@@ -1683,9 +1678,13 @@ final class TDSChannel implements Serializable {
                         if (logger.isLoggable(Level.FINEST))
                             logger.finest(toString() + " Loading key store");
 
+                        char[] trustStorePassword = SecureStringUtil.getInstance()
+                                .getDecryptedChars(con.encryptedTrustStorePassword);
                         try {
-                            ks.load(is, (null == trustStorePassword) ? null : trustStorePassword.toCharArray());
+                            ks.load(is, (null == trustStorePassword) ? null : trustStorePassword);
                         } finally {
+                            if (trustStorePassword != null)
+                                Arrays.fill(trustStorePassword, ' ');
                             // We are also done with the trust store input stream.
                             if (null != is) {
                                 try {
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java
index 5a352cc5..7e36367e 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java
@@ -218,7 +218,7 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
     /** flag indicating whether prelogin TLS handshake is required */
     private boolean isTDSS = false;
 
-    String encryptedTrustStorePassword = null;
+    byte[] encryptedTrustStorePassword = null;
 
     /**
      * Return an existing cached SharedTimer associated with this Connection or create a new one.
@@ -1844,7 +1844,8 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
                 String trustStorePassword = activeConnectionProperties
                         .getProperty(SQLServerDriverStringProperty.TRUST_STORE_PASSWORD.toString());
                 if (trustStorePassword != null) {
-                    encryptedTrustStorePassword = SecureStringUtil.getInstance().getEncryptedString(trustStorePassword);
+                    encryptedTrustStorePassword = SecureStringUtil.getInstance()
+                            .getEncryptedBytes(trustStorePassword.toCharArray());
                     activeConnectionProperties.remove(SQLServerDriverStringProperty.TRUST_STORE_PASSWORD.toString());
                 }
 
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SecureStringUtil.java b/src/main/java/com/microsoft/sqlserver/jdbc/SecureStringUtil.java
index c34979d1..0b5de8d8 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SecureStringUtil.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SecureStringUtil.java
@@ -2,7 +2,7 @@ package com.microsoft.sqlserver.jdbc;
 
 import java.security.SecureRandom;
 import java.text.MessageFormat;
-import java.util.Base64;
+import java.util.Arrays;
 
 import javax.crypto.Cipher;
 import javax.crypto.KeyGenerator;
@@ -88,7 +88,7 @@ final class SecureStringUtil {
     /**
      * Get encrypted value of given string
      * 
-     * @param str
+     * @param chars
      *        string to encrypt
      * 
      * @return encrypted string
@@ -96,7 +96,9 @@ final class SecureStringUtil {
      * @throws SQLServerException
      *         if error
      */
-    String getEncryptedString(String str) throws SQLServerException {
+    byte[] getEncryptedBytes(char[] chars) throws SQLServerException {
+        if (chars == null)
+            return null;
         SecureRandom random = new SecureRandom();
         random.nextBytes(iv);
         GCMParameterSpec ivParamSpec = new GCMParameterSpec(TAG_LENGTH * 8, iv);
@@ -104,8 +106,8 @@ final class SecureStringUtil {
         try {
             encryptCipher.init(Cipher.ENCRYPT_MODE, secretKey, ivParamSpec);
 
-            byte[] cipherText = encryptCipher.doFinal(str.getBytes());
-            return Base64.getEncoder().encodeToString(cipherText);
+            byte[] cipherText = encryptCipher.doFinal(Util.charsToBytes(chars));
+            return cipherText;
         } catch (Exception e) {
             MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_EncryptionFailed"));
             Object[] msgArgs = {e.getMessage()};
@@ -116,24 +118,31 @@ final class SecureStringUtil {
     /**
      * Get decrypted value of an encrypted string
      * 
-     * @param str
+     * @param bytes
      * 
      * @return decrypted string
      * 
      * @throws SQLServerException
      */
-    String getDecryptedString(String str) throws SQLServerException {
+    char[] getDecryptedChars(byte[] bytes) throws SQLServerException {
+        if (bytes == null)
+            return null;
         GCMParameterSpec ivParamSpec = new GCMParameterSpec(TAG_LENGTH * 8, iv);
 
+        byte[] plainText = null;
         try {
             decryptCipher.init(Cipher.DECRYPT_MODE, secretKey, ivParamSpec);
 
-            byte[] plainText = decryptCipher.doFinal(Base64.getDecoder().decode(str));
-            return new String(plainText);
+            plainText = decryptCipher.doFinal(bytes);
+            return Util.bytesToChars(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);
+        } finally {
+            if (plainText != null) {
+                Arrays.fill(plainText, (byte) 0);
+            }
         }
     }
 }
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/Util.java b/src/main/java/com/microsoft/sqlserver/jdbc/Util.java
index 9c97bd1b..6dba1393 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/Util.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/Util.java
@@ -1006,6 +1006,27 @@ final class Util {
     static String zeroOneToYesNo(int i) {
         return 0 == i ? "NO" : "YES";
     }
+
+    static byte[] charsToBytes(char[] chars) {
+        if (chars == null)
+            return null;
+        byte[] bytes = new byte[chars.length * 2];
+        for (int i = 0; i < chars.length; i++) {
+            bytes[i * 2] = (byte) (0xff & (chars[i] >> 8));
+            bytes[i * 2 + 1] = (byte) (0xff & (chars[i]));
+        }
+        return bytes;
+    }
+
+    static char[] bytesToChars(byte[] bytes) {
+        if (bytes == null)
+            return null;
+        char[] chars = new char[bytes.length / 2];
+        for (int i = 0; i < chars.length; i++) {
+            chars[i] = (char) (((0xFF & (bytes[i * 2])) << 8) | (0xFF & bytes[i * 2 + 1]));
+        }
+        return chars;
+    }
 }
 
 


Test patch:
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/SSLCertificateValidation.java b/src/test/java/com/microsoft/sqlserver/jdbc/SSLCertificateValidationTest.java
similarity index 79%
rename from src/test/java/com/microsoft/sqlserver/jdbc/SSLCertificateValidation.java
rename to src/test/java/com/microsoft/sqlserver/jdbc/SSLCertificateValidationTest.java
index 101cfb4b..1f0de5d9 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/SSLCertificateValidation.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/SSLCertificateValidationTest.java
@@ -17,7 +17,7 @@ import org.junit.runner.RunWith;
 
 
 @RunWith(JUnitPlatform.class)
-public class SSLCertificateValidation {
+public class SSLCertificateValidationTest {
 
     /**
      * Tests our internal method, validateServerName() against different possible names in SSL certificate.
@@ -33,106 +33,102 @@ public class SSLCertificateValidation {
 
         // Set up the HostNameOverrideX509TrustManager object using reflection
         TDSChannel tdsc = new TDSChannel(new SQLServerConnection("someConnectionProperty"));
-        Class<?> hsoClass = Class.forName("com.microsoft.sqlserver.jdbc.TDSChannel$HostNameOverrideX509TrustManager");
+        Class<?> hsoClass = Class.forName("com.microsoft.sqlserver.jdbc.HostNameOverrideX509TrustManager");
         Constructor<?> constructor = hsoClass.getDeclaredConstructors()[0];
         constructor.setAccessible(true);
-        Object hsoObject = constructor.newInstance(null, tdsc, null, serverName);
-        Method method = hsoObject.getClass().getDeclaredMethod("validateServerName", String.class);
+        Object hsoObject = constructor.newInstance(tdsc, null, serverName);
+        Method method = SQLServerCertificateUtils.class.getDeclaredMethod("validateServerName", String.class, String.class);
         method.setAccessible(true);
 
         /*
          * Server Name = msjdbc.database.windows.net SAN = msjdbc.database.windows.net Expected result: true
          */
-        assertTrue((boolean) method.invoke(hsoObject, "msjdbc.database.windows.net"));
+        assertTrue((boolean) method.invoke(hsoObject, "msjdbc.database.windows.net", serverName));
 
         /*
          * Server Name = msjdbc.database.windows.net SAN = msjdbc***.database.windows.net Expected result: true
          */
-        assertTrue((boolean) method.invoke(hsoObject, "msjdbc***.database.windows.net"));
+        assertTrue((boolean) method.invoke(hsoObject, "msjdbc***.database.windows.net", serverName));
 
         /*
          * Server Name = msjdbc.database.windows.net SAN = ms*bc.database.windows.net Expected result: true
          */
-        assertTrue((boolean) method.invoke(hsoObject, "ms*bc.database.windows.net"));
+        assertTrue((boolean) method.invoke(hsoObject, "ms*bc.database.windows.net", serverName));
 
         /*
          * Server Name = msjdbc.database.windows.net SAN = *bc.database.windows.net Expected result: true
          */
-        assertTrue((boolean) method.invoke(hsoObject, "*bc.database.windows.net"));
+        assertTrue((boolean) method.invoke(hsoObject, "*bc.database.windows.net", serverName));
 
         /*
          * Server Name = msjdbc.database.windows.net SAN = ms*.database.windows.net Expected result: true
          */
-        assertTrue((boolean) method.invoke(hsoObject, "ms*.database.windows.net"));
+        assertTrue((boolean) method.invoke(hsoObject, "ms*.database.windows.net", serverName));
 
         /*
          * Server Name = msjdbc.database.windows.net SAN = *jd*.database.windows.net Expected result: true
          */
-        assertTrue((boolean) method.invoke(hsoObject, "*jd*.database.windows.net"));
+        assertTrue((boolean) method.invoke(hsoObject, "*jd*.database.windows.net", serverName));
 
         /*
          * Server Name = msjdbc.database.windows.net SAN = ms.*.net Expected result: false
          */
-        assertFalse((boolean) method.invoke(hsoObject, "ms.*.net"));
+        assertFalse((boolean) method.invoke(hsoObject, "ms.*.net", serverName));
 
         /*
          * Server Name = msjdbc.database.windows.net SAN = msjdbc.asd*dsa.windows.net Expected result: false
          */
-        assertFalse((boolean) method.invoke(hsoObject, "msjdbc.asd*dsa.windows.net"));
+        assertFalse((boolean) method.invoke(hsoObject, "msjdbc.asd*dsa.windows.net", serverName));
 
         /*
          * Server Name = msjdbc.database.windows.net SAN = *.*.windows.net Expected result: false
          */
-        assertFalse((boolean) method.invoke(hsoObject, ".*.windows.net"));
+        assertFalse((boolean) method.invoke(hsoObject, ".*.windows.net", serverName));
 
         /*
          * Server Name = msjdbc.database.windows.net SAN = msjdbc.*.windows.net Expected result: false
          */
-        assertFalse((boolean) method.invoke(hsoObject, "msjdbc.*.windows.net"));
+        assertFalse((boolean) method.invoke(hsoObject, "msjdbc.*.windows.net", serverName));
 
         /*
          * Server Name = msjdbc.database.windows.net SAN = *.*.windows.net Expected result: false Note: multiple
          * wildcards are not allowed, so this case shouldn't happen, but we still make sure to fail this.
          */
-        assertFalse((boolean) method.invoke(hsoObject, "*.*.windows.net"));
+        assertFalse((boolean) method.invoke(hsoObject, "*.*.windows.net", serverName));
 
         /*
          * Server Name = msjdbc.database.windows.net SAN = *.com Expected result: false A cert with * plus a top-level
          * domain is not allowed.
          */
-        assertFalse((boolean) method.invoke(hsoObject, "*.com"));
+        assertFalse((boolean) method.invoke(hsoObject, "*.com", serverName));
 
         /*
          * Server Name = msjdbc.database.windows.net SAN = xn--ms*.database.windows.net Expected result: false
          */
-        assertFalse((boolean) method.invoke(hsoObject, "xn--ms*.database.windows.net"));
+        assertFalse((boolean) method.invoke(hsoObject, "xn--ms*.database.windows.net", serverName));
 
         /*
          * Server Name = msjdbc.database.windows.net SAN = * Expected result: false
          */
-        assertFalse((boolean) method.invoke(hsoObject, "*"));
+        assertFalse((boolean) method.invoke(hsoObject, "*", serverName));
 
         /*
          * Server Name = msjdbc.database.windows.net SAN = ms*atabase.windows.net Expected result: false
          */
-        assertFalse((boolean) method.invoke(hsoObject, "ms*atabase.windows.net"));
+        assertFalse((boolean) method.invoke(hsoObject, "ms*atabase.windows.net", serverName));
 
-        hsoObject = constructor.newInstance(null, tdsc, null, serverName2);
-        method = hsoObject.getClass().getDeclaredMethod("validateServerName", String.class);
-        method.setAccessible(true);
+        hsoObject = constructor.newInstance(tdsc, null, serverName2);
 
         /*
          * Server Name = bbbbuuzzuzzzzzz.example.net SAN = b*zzz.example.net Expected result: true
          */
-        assertTrue((boolean) method.invoke(hsoObject, "b*zzz.example.net"));
+        assertTrue((boolean) method.invoke(hsoObject, "b*zzz.example.net", serverName2));
 
-        hsoObject = constructor.newInstance(null, tdsc, null, serverName3);
-        method = hsoObject.getClass().getDeclaredMethod("validateServerName", String.class);
-        method.setAccessible(true);
+        hsoObject = constructor.newInstance(tdsc, null, serverName3);
 
         /*
          * Server Name = xn--ms.database.windows.net SAN = xn--ms.database.windows.net Expected result: true
          */
-        assertTrue((boolean) method.invoke(hsoObject, "xn--ms.database.windows.net"));
+        assertTrue((boolean) method.invoke(hsoObject, "xn--ms.database.windows.net", serverName3));
     }
 }
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/UtilTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/UtilTest.java
index 31932d9d..bddd9a26 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/UtilTest.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/UtilTest.java
@@ -5,6 +5,7 @@
 package com.microsoft.sqlserver.jdbc;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
 
 import java.sql.SQLException;
 import java.util.Properties;
@@ -64,6 +65,25 @@ public class UtilTest {
         assertEquals(prt.getProperty("password"), "pasS}");
     }
 
+    private static String testString = "A ß € 嗨 𝄞 🙂ăѣ𝔠ծềſģȟᎥ𝒋ǩľḿꞑȯ𝘱𝑞𝗋𝘴ȶ𝞄𝜈ψ𝒙𝘆𝚣1234567890!@#$%^&*()-_=+[{]};:'\",<.>/?~𝘈Ḇ𝖢𝕯٤ḞԍНǏ𝙅ƘԸⲘ𝙉০Ρ𝗤Ɍ𝓢ȚЦ𝒱Ѡ𝓧ƳȤѧᖯć𝗱ễ𝑓𝙜Ⴙ𝞲𝑗𝒌ļṃŉо𝞎𝒒ᵲꜱ𝙩ừ𝗏ŵ𝒙𝒚ź1234567890!@#$%^&*()-_=+[{]};:'\",<.>/?~АḂⲤ𝗗𝖤𝗙ꞠꓧȊ𝐉𝜥ꓡ𝑀𝑵Ǭ𝙿𝑄Ŗ𝑆𝒯𝖴𝘝𝘞ꓫŸ𝜡ả𝘢ƀ𝖼ḋếᵮℊ𝙝Ꭵ𝕛кιṃդⱺ𝓅𝘲𝕣𝖘ŧ𝑢ṽẉ𝘅ყž1234567890!@#$%^&*()-_=+[{]};:'\",<.>/?~Ѧ𝙱ƇᗞΣℱԍҤ١𝔍К𝓛𝓜ƝȎ𝚸𝑄Ṛ𝓢ṮṺƲᏔꓫ𝚈𝚭𝜶Ꮟçძ𝑒𝖿𝗀ḧ𝗂𝐣ҝɭḿ𝕟𝐨𝝔𝕢ṛ𝓼тú𝔳ẃ⤬𝝲𝗓1234567890!@#$%^&*()-_=+[{]};:'\",<.>/?~𝖠Β𝒞𝘋𝙴𝓕ĢȞỈ𝕵ꓗʟ𝙼ℕ০𝚸𝗤ՀꓢṰǓⅤ𝔚Ⲭ𝑌𝙕𝘢𝕤";
+
+    @Test
+    public void testArrayConversions() {
+        char[] chars = testString.toCharArray();
+        byte[] bytes = Util.charsToBytes(chars);
+        char[] newChars = Util.bytesToChars(bytes);
+        assertArrayEquals(chars, newChars);
+        String end = String.valueOf(newChars);
+        assertEquals(testString, end);
+    }
+
+    @Test
+    public void testSecureStringUtil() throws SQLException {
+        byte[] bytes = SecureStringUtil.getInstance().getEncryptedBytes(testString.toCharArray());
+        String end = String.valueOf(SecureStringUtil.getInstance().getDecryptedChars(bytes));
+        assertEquals(testString, end);
+    }
+
     private void writeAndReadLong(long valueToTest) {
         byte[] buffer = new byte[8];
         Util.writeLong(valueToTest, buffer, 0);