Bug 509604 - backward compatibility improvement
- Fixed a backward compatibility issue in core
- Added regression testing

Reviewed-by: Lukas Jungmann <lukas.jungmann@oracle.com>
Signed-off-by: David Minsky <david.minsky@oracle.com>
diff --git a/foundation/eclipselink.core.test/src/org/eclipse/persistence/testing/tests/security/SecurableBackwardsCompatibilityTest.java b/foundation/eclipselink.core.test/src/org/eclipse/persistence/testing/tests/security/SecurableBackwardsCompatibilityTest.java
new file mode 100644
index 0000000..f29721c
--- /dev/null
+++ b/foundation/eclipselink.core.test/src/org/eclipse/persistence/testing/tests/security/SecurableBackwardsCompatibilityTest.java
@@ -0,0 +1,170 @@
+/*******************************************************************************
+ * Copyright (c) 1998, 2016 Oracle and/or its affiliates. All rights reserved.
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 and Eclipse Distribution License v. 1.0
+ * which accompanies this distribution.
+ * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html
+ * and the Eclipse Distribution License is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * Contributors:
+ *     dminsky - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.persistence.testing.tests.security;
+
+import java.io.ByteArrayOutputStream;
+import java.io.ObjectOutputStream;
+
+import javax.crypto.Cipher;
+import javax.crypto.CipherOutputStream;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.DESKeySpec;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.eclipse.persistence.exceptions.ValidationException;
+import org.eclipse.persistence.internal.helper.Helper;
+import org.eclipse.persistence.internal.security.JCEEncryptor;
+import org.eclipse.persistence.internal.security.Securable;
+
+import org.eclipse.persistence.testing.framework.ReflectionHelper;
+import org.eclipse.persistence.testing.framework.TestCase;
+
+/**
+ * Regression test suite for the EclipseLink reference implementation of the Securable interface
+ * @author dminsky
+ */
+public class SecurableBackwardsCompatibilityTest extends TestCase {
+    
+    public SecurableBackwardsCompatibilityTest(String testMethod) {
+        super();
+        setName(testMethod);
+    }
+    
+    @Override
+    public void test() throws Throwable {
+        ReflectionHelper.invokeMethod(getName(), this, new Class[0], new Object[0]);
+    }
+    
+    /**
+     * Test the decryption of a String encrypted with DES ECB.
+     * @throws Exception
+     */
+    public void testStringDecryption_DES_ECB() throws Exception {
+        String plainTextString = "welcome123_des_ecb";
+        
+        String testString = encryptString_DES_ECB(plainTextString);
+        assertFalse("Strings should not match.", plainTextString.equals(testString));
+        
+        Securable securable = new JCEEncryptor();
+        String decryptedString = securable.decryptPassword(testString);
+        assertEquals("Strings should match.", plainTextString, decryptedString);
+    }
+    
+    /**
+     * Test the decryption of a String encrypted with AES CBC.
+     * @throws Exception
+     */
+    public void testStringDecryption_AES_CBC() throws Exception {
+        String plainTextString = "welcome123_aes_cbc";
+
+        Securable securable = new JCEEncryptor();
+        String testString = securable.encryptPassword(plainTextString);
+        assertFalse("Strings should not match.", plainTextString.equals(testString));
+        
+        String decryptedString = securable.decryptPassword(testString);
+        assertEquals("Strings should match.", plainTextString, decryptedString);
+    }
+    
+    /**
+     * Test the decryption of a String encrypted with AES ECB.
+     * @throws Exception
+     */
+    public void testStringDecryption_AES_ECB() throws Exception {
+        String plainTextString = "welcome123_aes_ecb";
+        
+        String testString = encryptString_AES_ECB(plainTextString);
+        assertFalse("Strings should not match.", plainTextString.equals(testString));
+        
+        Securable securable = new JCEEncryptor();
+        String decryptedString = securable.decryptPassword(testString);
+        assertEquals("Strings should match.", plainTextString, decryptedString);
+    }
+    
+    /**
+     * Test the decryption/processing of a plaintext String.
+     * @throws Exception
+     */
+    public void testStringDecryption_PlainText() throws Exception {
+        String plainTextString = "welcome123_plaintext";
+        
+        Securable securable = new JCEEncryptor();
+        String decryptedString = securable.decryptPassword(plainTextString);
+        assertEquals("Passwords should match.", plainTextString, decryptedString);
+    }
+    
+    /**
+     * Test the decryption/processing of a null parameter.
+     * @throws Exception
+     */
+    public void testNullParameterDecryption() throws Exception {
+        Securable securable = new JCEEncryptor();
+        String returnValue = securable.decryptPassword(null);
+        assertNull("Null should be returned when decrypting a null value", returnValue);
+    }
+    
+    /**
+     * Test the encryption of a null parameter.
+     * @throws Exception
+     */
+    public void testNullParameterEncryption() throws Exception {
+        ValidationException expectedException = null;
+        try {
+            Securable securable = new JCEEncryptor();
+            securable.encryptPassword(null);
+        } catch (ValidationException ve) {
+            expectedException = ve;
+        }
+        assertNotNull("A ValidationException should be thrown when encrypting a null value", expectedException);
+    }
+    
+    /*
+     * Internal test utility:
+     * Return a DES ECB encrypted version of the String parameter, using the legacy encryption code.
+     */
+    private String encryptString_DES_ECB(String aString) throws Exception {
+        final byte[] bytes = Helper.buildBytesFromHexString("E60B80C7AEC78038");
+        
+        Cipher cipher = Cipher.getInstance("DES/ECB/PKCS5Padding");
+        cipher.init(Cipher.ENCRYPT_MODE, SecretKeyFactory.getInstance("DES").generateSecret(new DESKeySpec(bytes)));
+        
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        CipherOutputStream cos = new CipherOutputStream(baos, cipher);
+        ObjectOutputStream oos = new ObjectOutputStream(cos);
+        oos.writeObject(aString);
+        oos.flush();
+        oos.close();
+
+        return Helper.buildHexStringFromBytes(baos.toByteArray());
+    }
+    
+    /*
+     * Internal test utility:
+     * Return an AES ECB encrypted version of the String parameter, using the legacy encryption code.
+     */
+    private String encryptString_AES_ECB(String aString) throws Exception {
+        final byte[] bytes = Helper.buildBytesFromHexString("3E7CFEF156E712906E1F603B59463C67");
+        
+        Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
+        cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(bytes, "AES"));
+        
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        CipherOutputStream cos = new CipherOutputStream(baos, cipher);
+        ObjectOutputStream oos = new ObjectOutputStream(cos);
+        oos.writeObject(aString);
+        oos.flush();
+        oos.close();
+
+        return Helper.buildHexStringFromBytes(baos.toByteArray());
+    }
+    
+}
diff --git a/foundation/eclipselink.core.test/src/org/eclipse/persistence/testing/tests/security/SecurityTestModel.java b/foundation/eclipselink.core.test/src/org/eclipse/persistence/testing/tests/security/SecurityTestModel.java
index ab83a15..232971a 100644
--- a/foundation/eclipselink.core.test/src/org/eclipse/persistence/testing/tests/security/SecurityTestModel.java
+++ b/foundation/eclipselink.core.test/src/org/eclipse/persistence/testing/tests/security/SecurityTestModel.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 1998, 2015 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1998, 2016 Oracle and/or its affiliates. All rights reserved.
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Public License v1.0 and Eclipse Distribution License v. 1.0
  * which accompanies this distribution.
@@ -44,6 +44,7 @@ public void addTests() {
     addTest(getJCETestSuite());
     addTest(getValidationSecurityTestSuite());
     addTest(new DatabaseLoginWithNoEncryptorTest());
+    addTest(getSecurableBackwardsCompatibilityTestSuite());
   }
 
   public static TestSuite getJCETestSuite() {
@@ -76,6 +77,21 @@ public static TestSuite getValidationSecurityTestSuite() {
 
         return suite;
     }
+    
+    public static TestSuite getSecurableBackwardsCompatibilityTestSuite() {
+        TestSuite suite = new TestSuite();
+        suite.setName("Securable Backward Compatibility Tests");
+        suite.setDescription("Tests the backwards compatibility of the (default) Securable reference implementation");
+        
+        suite.addTest(new SecurableBackwardsCompatibilityTest("testStringDecryption_PlainText"));
+        suite.addTest(new SecurableBackwardsCompatibilityTest("testStringDecryption_DES_ECB"));
+        suite.addTest(new SecurableBackwardsCompatibilityTest("testStringDecryption_AES_CBC"));
+        suite.addTest(new SecurableBackwardsCompatibilityTest("testStringDecryption_AES_ECB"));
+        suite.addTest(new SecurableBackwardsCompatibilityTest("testNullParameterDecryption"));
+        suite.addTest(new SecurableBackwardsCompatibilityTest("testNullParameterEncryption"));
+        
+        return suite;
+    }
 
     /**
      * Return the JUnit suite to allow JUnit runner to find it.
diff --git a/foundation/org.eclipse.persistence.core/src/org/eclipse/persistence/internal/security/JCEEncryptor.java b/foundation/org.eclipse.persistence.core/src/org/eclipse/persistence/internal/security/JCEEncryptor.java
index 9adc3c0..3c9e418 100644
--- a/foundation/org.eclipse.persistence.core/src/org/eclipse/persistence/internal/security/JCEEncryptor.java
+++ b/foundation/org.eclipse.persistence.core/src/org/eclipse/persistence/internal/security/JCEEncryptor.java
@@ -12,7 +12,12 @@
  ******************************************************************************/
 package org.eclipse.persistence.internal.security;
 
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+
 import javax.crypto.Cipher;
+import javax.crypto.CipherInputStream;
 import javax.crypto.SecretKey;
 import javax.crypto.SecretKeyFactory;
 import javax.crypto.spec.DESKeySpec;
@@ -24,20 +29,21 @@
 import org.eclipse.persistence.internal.helper.Helper;
 
 /**
- * TopLink reference implementation for password encryption.
+ * EclipseLink reference implementation for password encryption.
  *
  * @author Guy Pelletier
  */
 public class JCEEncryptor implements Securable {
-    // Legacy decrypt cipher used for backwards compatibility only.
-    private static final String DES = "DES/ECB/PKCS5Padding";
-    private final Cipher decryptCipherDES;
+    
+    // Legacy DES ECB cipher used for backwards compatibility decryption only.
+    private static final String DES_ECB = "DES/ECB/PKCS5Padding";
+    private final Cipher decryptCipherDES_ECB;
 
-    // Legacy AES cipher used for backwards compatibility only.
-    private static final String AES = "AES/ECB/PKCS5Padding";
-    private final Cipher decryptCipherAES;
+    // Legacy AES ECB cipher used for backwards compatibility decryption only.
+    private static final String AES_ECB = "AES/ECB/PKCS5Padding";
+    private final Cipher decryptCipherAES_ECB;
 
-    // All encryption is done through the AES cipher.
+    // All encryption is done through the AES CBC cipher.
     private static final String AES_CBC = "AES/CBC/PKCS5Padding";
     private final Cipher encryptCipherAES_CBC;
     private final Cipher decryptCipherAES_CBC;
@@ -54,11 +60,11 @@ public JCEEncryptor() throws Exception {
          *
          * Confusing??? Well, don't move this code before talking to Guy first!
          */
-        decryptCipherDES = Cipher.getInstance(DES);
-        decryptCipherDES.init(Cipher.DECRYPT_MODE, Synergizer.getDESMultitasker());
+        decryptCipherDES_ECB = Cipher.getInstance(DES_ECB);
+        decryptCipherDES_ECB.init(Cipher.DECRYPT_MODE, Synergizer.getDESMultitasker());
 
-        decryptCipherAES = Cipher.getInstance(AES);
-        decryptCipherAES.init(Cipher.DECRYPT_MODE, Synergizer.getAESMultitasker());
+        decryptCipherAES_ECB = Cipher.getInstance(AES_ECB);
+        decryptCipherAES_ECB.init(Cipher.DECRYPT_MODE, Synergizer.getAESMultitasker());
 
         SecretKey sk = Synergizer.getAESCBCMultitasker();
         IvParameterSpec iv = Synergizer.getIvSpec();
@@ -79,7 +85,6 @@ public synchronized String encryptPassword(String password) {
         } catch (Exception e) {
             throw ValidationException.errorEncryptingPassword(e);
         }
-
     }
 
     /**
@@ -88,32 +93,50 @@ public synchronized String encryptPassword(String password) {
      */
     @Override
     public synchronized String decryptPassword(String encryptedPswd) {
-        String password = null;
+        if (encryptedPswd == null) { 
+            return null;
+        }
 
-        if (encryptedPswd != null) {
-            byte[] bytePassword = new byte[0];
+        String password = null;
+        byte[] bytePassword = new byte[0];
+        
+        try {
+            bytePassword = Helper.buildBytesFromHexString(encryptedPswd);
+            // try AES/CBC first
+            password = new String(decryptCipherAES_CBC.doFinal(bytePassword), "UTF-8");
+        } catch (ConversionException ce) {
+            // buildBytesFromHexString failed, assume clear text
+            password = encryptedPswd;
+        } catch (Exception e) {
+            ObjectInputStream oisAes = null;
             try {
-                bytePassword = Helper.buildBytesFromHexString(encryptedPswd);
-                password = new String(decryptCipherAES_CBC.doFinal(bytePassword), "UTF-8");
-            } catch (ConversionException ce) {
-                // Never prepared (buildBytesFromHexString failed), assume clear text
-                password = encryptedPswd;
+                // try AES/ECB second
+                oisAes = new ObjectInputStream(new CipherInputStream(new ByteArrayInputStream(bytePassword), decryptCipherAES_ECB));
+                password = (String)oisAes.readObject();
             } catch (Exception f) {
+                ObjectInputStream oisDes = null;
                 try {
-                    // try AES/ECB
-                    password = new String(decryptCipherAES.doFinal(bytePassword));
-                } catch (Exception exc) {
-                    // Catch all exceptions when trying to decrypt using AES and try the
-                    // old DES decryptor before deciding what to do.
-                    try {
-                        password = new String(decryptCipherDES.doFinal(bytePassword));
-                    } catch (ArrayIndexOutOfBoundsException e) {
-                        // JCE 1.2.1 couldn't decrypt it, assume clear text
-                        password = encryptedPswd;
-                    } catch (Exception e) {
-                        throw ValidationException.errorDecryptingPassword(e);
+                    // try DES/ECB third
+                    oisDes = new ObjectInputStream(new CipherInputStream(new ByteArrayInputStream(bytePassword), decryptCipherDES_ECB));
+                    password = (String)oisDes.readObject();
+                } catch (ArrayIndexOutOfBoundsException g) {
+                    // JCE 1.2.1 couldn't decrypt it, assume clear text
+                    password = encryptedPswd;
+                } catch (Exception h) {
+                    throw ValidationException.errorDecryptingPassword(h);
+                } finally {
+                    if (oisDes != null) {
+                        try {
+                            oisDes.close();
+                        } catch (IOException e2) {} 
                     }
                 }
+            } finally {
+                if (oisAes != null) {
+                    try {
+                        oisAes.close();
+                    } catch (IOException e1) {} 
+                }
             }
         }