Repository: microsoft/mssql-jdbc
Pull request #1858 (2022-06-30 18:40:41)

Fix: Initialization vector cannot be reused
Prepend iv onto the encrypted bytes so that each iv is unique and preserved with its encrypted bytes for later decryption.


Patch:
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SecureStringUtil.java b/src/main/java/com/microsoft/sqlserver/jdbc/SecureStringUtil.java
index 0b5de8d8..2ed97efb 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SecureStringUtil.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SecureStringUtil.java
@@ -31,9 +31,6 @@ final class SecureStringUtil {
     /* authentication tag length in bits */
     static final int TAG_LENGTH = 16;
 
-    /* initialization vector */
-    byte[] iv;
-
     /** secret key for encryption/decryption */
     SecretKeySpec secretKey;
 
@@ -68,7 +65,6 @@ final class SecureStringUtil {
      *         if error
      */
     private SecureStringUtil() throws SQLServerException {
-        iv = new byte[IV_LENGTH];
         try {
             // generate key */
             KeyGenerator keygen = KeyGenerator.getInstance(KEYGEN_ALGORITHEM);
@@ -99,6 +95,8 @@ final class SecureStringUtil {
     byte[] getEncryptedBytes(char[] chars) throws SQLServerException {
         if (chars == null)
             return null;
+
+        byte[] iv = new byte[IV_LENGTH];
         SecureRandom random = new SecureRandom();
         random.nextBytes(iv);
         GCMParameterSpec ivParamSpec = new GCMParameterSpec(TAG_LENGTH * 8, iv);
@@ -107,7 +105,10 @@ final class SecureStringUtil {
             encryptCipher.init(Cipher.ENCRYPT_MODE, secretKey, ivParamSpec);
 
             byte[] cipherText = encryptCipher.doFinal(Util.charsToBytes(chars));
-            return cipherText;
+            byte[] bytes = new byte[iv.length + cipherText.length];
+            System.arraycopy(iv, 0, bytes, 0, iv.length);
+            System.arraycopy(cipherText, 0, bytes, iv.length, cipherText.length);
+            return bytes;
         } catch (Exception e) {
             MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_EncryptionFailed"));
             Object[] msgArgs = {e.getMessage()};
@@ -127,13 +128,17 @@ final class SecureStringUtil {
     char[] getDecryptedChars(byte[] bytes) throws SQLServerException {
         if (bytes == null)
             return null;
+
+        byte[] iv = new byte[IV_LENGTH];
+        System.arraycopy(bytes, 0, iv, 0, IV_LENGTH);
+
         GCMParameterSpec ivParamSpec = new GCMParameterSpec(TAG_LENGTH * 8, iv);
 
         byte[] plainText = null;
         try {
             decryptCipher.init(Cipher.DECRYPT_MODE, secretKey, ivParamSpec);
 
-            plainText = decryptCipher.doFinal(bytes);
+            plainText = decryptCipher.doFinal(bytes, IV_LENGTH, bytes.length - IV_LENGTH);
             return Util.bytesToChars(plainText);
         } catch (Exception e) {
             MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_DecryptionFailed"));


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 bddd9a26..835bab8e 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/UtilTest.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/UtilTest.java
@@ -66,6 +66,8 @@ public class UtilTest {
     }
 
     private static String testString = "A ß € 嗨 𝄞 🙂ăѣ𝔠ծềſģȟᎥ𝒋ǩľḿꞑȯ𝘱𝑞𝗋𝘴ȶ𝞄𝜈ψ𝒙𝘆𝚣1234567890!@#$%^&*()-_=+[{]};:'\",<.>/?~𝘈Ḇ𝖢𝕯٤ḞԍНǏ𝙅ƘԸⲘ𝙉০Ρ𝗤Ɍ𝓢ȚЦ𝒱Ѡ𝓧ƳȤѧᖯć𝗱ễ𝑓𝙜Ⴙ𝞲𝑗𝒌ļṃŉо𝞎𝒒ᵲꜱ𝙩ừ𝗏ŵ𝒙𝒚ź1234567890!@#$%^&*()-_=+[{]};:'\",<.>/?~АḂⲤ𝗗𝖤𝗙ꞠꓧȊ𝐉𝜥ꓡ𝑀𝑵Ǭ𝙿𝑄Ŗ𝑆𝒯𝖴𝘝𝘞ꓫŸ𝜡ả𝘢ƀ𝖼ḋếᵮℊ𝙝Ꭵ𝕛кιṃդⱺ𝓅𝘲𝕣𝖘ŧ𝑢ṽẉ𝘅ყž1234567890!@#$%^&*()-_=+[{]};:'\",<.>/?~Ѧ𝙱ƇᗞΣℱԍҤ١𝔍К𝓛𝓜ƝȎ𝚸𝑄Ṛ𝓢ṮṺƲᏔꓫ𝚈𝚭𝜶Ꮟçძ𝑒𝖿𝗀ḧ𝗂𝐣ҝɭḿ𝕟𝐨𝝔𝕢ṛ𝓼тú𝔳ẃ⤬𝝲𝗓1234567890!@#$%^&*()-_=+[{]};:'\",<.>/?~𝖠Β𝒞𝘋𝙴𝓕ĢȞỈ𝕵ꓗʟ𝙼ℕ০𝚸𝗤ՀꓢṰǓⅤ𝔚Ⲭ𝑌𝙕𝘢𝕤";
+    private static String testString2 = "ssdfsdflkjh9u0345)*&)(*&%$";
+    private static String testString3 = "ss345(*&^%oujdf.';lk2345(*&()*$#~!`1\\]wer><.,/?dfsdflkjh9u0345)*&)(*&%$";
 
     @Test
     public void testArrayConversions() {
@@ -79,9 +81,17 @@ public class UtilTest {
 
     @Test
     public void testSecureStringUtil() throws SQLException {
+        // Encrypt/decrypt multiple values in overlapping orders
         byte[] bytes = SecureStringUtil.getInstance().getEncryptedBytes(testString.toCharArray());
+        byte[] bytes2 = SecureStringUtil.getInstance().getEncryptedBytes(testString2.toCharArray());
         String end = String.valueOf(SecureStringUtil.getInstance().getDecryptedChars(bytes));
+        byte[] bytes3 = SecureStringUtil.getInstance().getEncryptedBytes(testString3.toCharArray());
+        String end3 = String.valueOf(SecureStringUtil.getInstance().getDecryptedChars(bytes3));
+        String end2 = String.valueOf(SecureStringUtil.getInstance().getDecryptedChars(bytes2));
+
         assertEquals(testString, end);
+        assertEquals(testString2, end2);
+        assertEquals(testString3, end3);
     }
 
     private void writeAndReadLong(long valueToTest) {