Try to get preferred external provider

When using an opaque key, try to honor the system's preferred provider
which is selected via late binding. If it's not found, try to find the
first provider that initializes correctly with the given key.

Bug: 21737886
Change-Id: I17483136aa5c1c5e474109525aefac9facaf7379
diff --git a/src/main/java/org/conscrypt/CryptoUpcalls.java b/src/main/java/org/conscrypt/CryptoUpcalls.java
index abd9063..a39aaad 100644
--- a/src/main/java/org/conscrypt/CryptoUpcalls.java
+++ b/src/main/java/org/conscrypt/CryptoUpcalls.java
@@ -16,11 +16,13 @@
 
 package org.conscrypt;
 
+import java.security.InvalidKeyException;
 import java.security.NoSuchAlgorithmException;
 import java.security.PrivateKey;
 import java.security.Provider;
 import java.security.Security;
 import java.security.Signature;
+import java.util.ArrayList;
 import javax.crypto.Cipher;
 import javax.crypto.NoSuchPaddingException;
 
@@ -33,22 +35,24 @@
     private CryptoUpcalls() {
     }
 
+    private static boolean isOurProvider(Provider p) {
+        return p.getClass().getPackage().equals(CryptoUpcalls.class.getPackage());
+    }
+
     /**
-     * Finds the first provider which provides {@code algorithm} but is not from
-     * the same ClassLoader as ours.
+     * Finds providers that are not us that provide the requested algorithms.
      */
-    public static Provider getExternalProvider(String algorithm) {
-        Provider selectedProvider = null;
+    private static ArrayList<Provider> getExternalProviders(String algorithm) {
+        ArrayList<Provider> providers = new ArrayList<>(1);
         for (Provider p : Security.getProviders(algorithm)) {
-            if (!p.getClass().getClassLoader().equals(CryptoUpcalls.class.getClassLoader())) {
-                selectedProvider = p;
-                break;
+            if (!isOurProvider(p)) {
+                providers.add(p);
             }
         }
-        if (selectedProvider == null) {
+        if (providers.isEmpty()) {
             System.err.println("Could not find external provider for algorithm: " + algorithm);
         }
-        return selectedProvider;
+        return providers;
     }
 
     public static byte[] rawSignDigestWithPrivateKey(PrivateKey javaKey, byte[] message) {
@@ -69,23 +73,47 @@
             throw new RuntimeException("Unexpected key type: " + javaKey.toString());
         }
 
-        Provider p = getExternalProvider("Signature." + algorithm);
-        if (p == null) {
-            return null;
-        }
-
-        // Get the Signature for this key.
         Signature signature;
+
+        // First try to get the most preferred provider as long as it isn't us.
         try {
-            signature = Signature.getInstance(algorithm, p);
+            signature = Signature.getInstance(algorithm);
+            signature.initSign(javaKey);
+
+            // Ignore it if it points back to us.
+            if (isOurProvider(signature.getProvider())) {
+                signature = null;
+            }
         } catch (NoSuchAlgorithmException e) {
             System.err.println("Unsupported signature algorithm: " + algorithm);
             return null;
+        } catch (InvalidKeyException e) {
+            System.err.println("Preferred provider doesn't support key:");
+            e.printStackTrace();
+            signature = null;
+        }
+
+        // If the preferred provider was us, fall back to trying to find the
+        // first not-us provider that initializes correctly.
+        if (signature == null) {
+            ArrayList<Provider> providers = getExternalProviders("Signature." + algorithm);
+            for (Provider p : providers) {
+                try {
+                    signature = Signature.getInstance(algorithm, p);
+                    signature.initSign(javaKey);
+                    break;
+                } catch (NoSuchAlgorithmException | InvalidKeyException e) {
+                    signature = null;
+                }
+            }
+            if (signature == null) {
+                System.err.println("Could not find provider for algorithm: " + algorithm);
+                return null;
+            }
         }
 
         // Sign the message.
         try {
-            signature.initSign(javaKey);
             signature.update(message);
             return signature.sign();
         } catch (Exception e) {
@@ -121,27 +149,47 @@
         }
 
         String transformation = "RSA/ECB/" + jcaPadding;
-        Provider p = getExternalProvider("Cipher." + transformation);
-        if (p == null) {
-            return null;
-        }
-
         Cipher c = null;
-        try {
-            c = Cipher.getInstance(transformation, p);
-        } catch (NoSuchAlgorithmException e) {
-            ;
-        } catch (NoSuchPaddingException e) {
-            ;
-        }
 
-        if (c == null) {
-            System.err.println("Unsupported transformation: " + transformation);
-            return null;
-        }
-
+        // First try to get the most preferred provider as long as it isn't us.
         try {
+            c = Cipher.getInstance(transformation);
             c.init(Cipher.DECRYPT_MODE, javaKey);
+
+            // Ignore it if it points back to us.
+            if (isOurProvider(c.getProvider())) {
+                c = null;
+            }
+        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
+            System.err.println("Unsupported cipher algorithm: " + transformation);
+            return null;
+        } catch (InvalidKeyException e) {
+            System.err.println("Preferred provider doesn't support key:");
+            e.printStackTrace();
+            c = null;
+        }
+
+        // If the preferred provider was us, fall back to trying to find the
+        // first not-us provider that initializes correctly.
+        if (c == null) {
+            ArrayList<Provider> providers = getExternalProviders("Cipher." + transformation);
+            for (Provider p : providers) {
+                try {
+                    c = Cipher.getInstance(transformation, p);
+                    c.init(Cipher.DECRYPT_MODE, javaKey);
+                    break;
+                } catch (NoSuchAlgorithmException | InvalidKeyException
+                        | NoSuchPaddingException e) {
+                    c = null;
+                }
+            }
+            if (c == null) {
+                System.err.println("Could not find provider for algorithm: " + transformation);
+                return null;
+            }
+        }
+
+        try {
             return c.doFinal(input);
         } catch (Exception e) {
             System.err.println("Exception while decrypting message with " + javaKey.getAlgorithm()