Repository: microsoft/mssql-jdbc
Pull request #1813 (2022-04-21 22:15:20)

Avoid String use with secure strings
Since Strings are immutable in Java and tend to stay around in memory for potential re-use in the JVM, it's best practice to avoid them for sensitive data. It's recommended to use char arrays instead and clear/overwrite them once they're no longer needed. This limits the amount of time sensitive data remains in memory.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4381f8c7..bb75ffe0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,14 +5,14 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/)
 
 ## [11.1.1] Preview Release
 ### Added
-- Added support for JDK 18 [1802](https://github.com/microsoft/mssql-jdbc/pull/1802)
-- Added support for bulk insert of null guid values [1778](https://github.com/microsoft/mssql-jdbc/pull/1778)
-- Added ALPN for TDSS connections [1795](https://github.com/microsoft/mssql-jdbc/pull/1795)
+- Added support for Java 18 [1802](https://github.com/microsoft/mssql-jdbc/pull/1802)
+- Added support for bulk insert of null GUID values [1778](https://github.com/microsoft/mssql-jdbc/pull/1778)
+- Added ALPN for TDS 8.0 connections [1795](https://github.com/microsoft/mssql-jdbc/pull/1795)
 ### Changed
 - Updated dependency versions of azure-keyvault and azure-identity [1798](https://github.com/microsoft/mssql-jdbc/pull/1798)
 - Refactored Idle Connection Resiliency timeout to use existing SharedTimer [1794](https://github.com/microsoft/mssql-jdbc/pull/1794)
 ### Fixed issues
-- Fixed check that mistakenly assigns wrong column encryption versions to attestation protocols [1805](https://github.com/microsoft/mssql-jdbc/pull/1805)
+- Fixed regression with Always Encrypted with secure enclaves [1805](https://github.com/microsoft/mssql-jdbc/pull/1805)
 
 
 ## [11.1.0] Preview Release
diff --git a/build.gradle b/build.gradle
index e7f52107..b003a871 100644
--- a/build.gradle
+++ b/build.gradle
@@ -11,7 +11,7 @@
 
 apply plugin: 'java'
 
-version = '11.1.1'
+version = '11.1.2-SNAPSHOT'
 def jreVersion = ""
 def testOutputDir = file("build/classes/java/test")
 def archivesBaseName = 'mssql-jdbc'
diff --git a/mssql-jdbc_auth_LICENSE b/mssql-jdbc_auth_LICENSE
index 777d2e5d..d28d6dab 100644
--- a/mssql-jdbc_auth_LICENSE
+++ b/mssql-jdbc_auth_LICENSE
@@ -1,5 +1,5 @@
 MICROSOFT SOFTWARE LICENSE TERMS
-MICROSOFT JDBC DRIVER 11.1.1 FOR SQL SERVER
+MICROSOFT JDBC DRIVER 11.1.2 FOR SQL SERVER
 
 These license terms are an agreement between you and Microsoft Corporation (or one of its affiliates). They apply to the software named above and any Microsoft services or software updates (except to the extent such services or updates are accompanied by new or additional terms, in which case those different terms apply prospectively and do not alter your or Microsoft’s rights relating to pre-updated software or services). IF YOU COMPLY WITH THESE LICENSE TERMS, YOU HAVE THE RIGHTS BELOW.  BY USING THE SOFTWARE, YOU ACCEPT THESE TERMS.
 
diff --git a/pom.xml b/pom.xml
index d1b133c4..4e763d4a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
 
 	<groupId>com.microsoft.sqlserver</groupId>
 	<artifactId>mssql-jdbc</artifactId>
-	<version>11.1.1</version>
+	<version>11.1.2-SNAPSHOT</version>
 	<packaging>jar</packaging>
 
 	<name>Microsoft JDBC Driver for SQL Server</name>
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/SQLJdbcVersion.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLJdbcVersion.java
index 5a44d328..430a8c09 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLJdbcVersion.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLJdbcVersion.java
@@ -8,7 +8,7 @@ package com.microsoft.sqlserver.jdbc;
 final class SQLJdbcVersion {
     static final int major = 11;
     static final int minor = 1;
-    static final int patch = 1;
+    static final int patch = 2;
     static final int build = 0;
     /*
      * Used to load mssql-jdbc_auth DLL.
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/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);