// Copyright 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. package org.chromium.chrome.browser; import android.app.Activity; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; import android.os.AsyncTask; import android.security.KeyChain; import android.security.KeyChainAliasCallback; import android.security.KeyChainException; import android.support.v7.app.AlertDialog; import android.util.Log; import org.chromium.base.ThreadUtils; import org.chromium.base.VisibleForTesting; import org.chromium.base.annotations.CalledByNative; import org.chromium.base.annotations.JNINamespace; import org.chromium.chrome.R; import org.chromium.ui.base.WindowAndroid; import java.security.Principal; import java.security.PrivateKey; import java.security.cert.CertificateEncodingException; import java.security.cert.X509Certificate; import javax.security.auth.x500.X500Principal; /** * Handles selection of client certificate on the Java side. This class is responsible for selection * of the client certificate to be used for authentication and retrieval of the private key and full * certificate chain. * * The entry point is selectClientCertificate() and it will be called on the UI thread. Then the * class will construct and run an appropriate CertAsyncTask, that will run in background, and * finally pass the results back to the UI thread, which will return to the native code. */ @JNINamespace("chrome::android") public class SSLClientCertificateRequest { static final String TAG = "SSLClientCertificateRequest"; /** * Implementation for anynchronous task of handling the certificate request. This * AsyncTask retrieves the authentication material from the system key store. * The key store is accessed in background, as the APIs being exercised * may be blocking. The results are posted back to native on the UI thread. */ private static class CertAsyncTaskKeyChain extends AsyncTask<Void, Void, Void> { // These fields will store the results computed in doInBackground so that they can be posted // back in onPostExecute. private byte[][] mEncodedChain; private PrivateKey mPrivateKey; // Pointer to the native certificate request needed to return the results. private final long mNativePtr; final Context mContext; final String mAlias; CertAsyncTaskKeyChain(Context context, long nativePtr, String alias) { mNativePtr = nativePtr; mContext = context; assert alias != null; mAlias = alias; } @Override protected Void doInBackground(Void... params) { String alias = getAlias(); if (alias == null) return null; PrivateKey key = getPrivateKey(alias); X509Certificate[] chain = getCertificateChain(alias); if (key == null || chain == null || chain.length == 0) { Log.w(TAG, "Empty client certificate chain?"); return null; } // Encode the certificate chain. byte[][] encodedChain = new byte[chain.length][]; try { for (int i = 0; i < chain.length; ++i) { encodedChain[i] = chain[i].getEncoded(); } } catch (CertificateEncodingException e) { Log.w(TAG, "Could not retrieve encoded certificate chain: " + e); return null; } mEncodedChain = encodedChain; mPrivateKey = key; return null; } @Override protected void onPostExecute(Void result) { ThreadUtils.assertOnUiThread(); nativeOnSystemRequestCompletion(mNativePtr, mEncodedChain, mPrivateKey); } private String getAlias() { return mAlias; } private PrivateKey getPrivateKey(String alias) { try { return KeyChain.getPrivateKey(mContext, alias); } catch (KeyChainException e) { Log.w(TAG, "KeyChainException when looking for '" + alias + "' certificate"); return null; } catch (InterruptedException e) { Log.w(TAG, "InterruptedException when looking for '" + alias + "'certificate"); return null; } } private X509Certificate[] getCertificateChain(String alias) { try { return KeyChain.getCertificateChain(mContext, alias); } catch (KeyChainException e) { Log.w(TAG, "KeyChainException when looking for '" + alias + "' certificate"); return null; } catch (InterruptedException e) { Log.w(TAG, "InterruptedException when looking for '" + alias + "'certificate"); return null; } } } /** * The system KeyChain API will call us back on the alias() method, passing the alias of the * certificate selected by the user. */ private static class KeyChainCertSelectionCallback implements KeyChainAliasCallback { private final long mNativePtr; private final Context mContext; KeyChainCertSelectionCallback(Context context, long nativePtr) { mContext = context; mNativePtr = nativePtr; } @Override public void alias(final String alias) { // This is called by KeyChainActivity in a background thread. Post task to // handle the certificate selection on the UI thread. ThreadUtils.runOnUiThread(new Runnable() { @Override public void run() { if (alias == null) { // No certificate was selected. ThreadUtils.runOnUiThread(new Runnable() { @Override public void run() { nativeOnSystemRequestCompletion(mNativePtr, null, null); } }); } else { new CertAsyncTaskKeyChain(mContext, mNativePtr, alias).execute(); } } }); } } /** * Wrapper class for the static KeyChain#choosePrivateKeyAlias method to facilitate testing. */ @VisibleForTesting static class KeyChainCertSelectionWrapper { private final Activity mActivity; private final KeyChainAliasCallback mCallback; private final String[] mKeyTypes; private final Principal[] mPrincipalsForCallback; private final String mHostName; private final int mPort; private final String mAlias; public KeyChainCertSelectionWrapper(Activity activity, KeyChainAliasCallback callback, String[] keyTypes, Principal[] principalsForCallback, String hostName, int port, String alias) { mActivity = activity; mCallback = callback; mKeyTypes = keyTypes; mPrincipalsForCallback = principalsForCallback; mHostName = hostName; mPort = port; mAlias = alias; } /** * Calls KeyChain#choosePrivateKeyAlias with the provided arguments. */ public void choosePrivateKeyAlias() throws ActivityNotFoundException { KeyChain.choosePrivateKeyAlias(mActivity, mCallback, mKeyTypes, mPrincipalsForCallback, mHostName, mPort, mAlias); } } /** * Dialog that explains to the user that client certificates aren't supported on their operating * system. Separated out into its own class to allow Robolectric unit testing of * maybeShowCertSelection without depending on Chrome resources. */ @VisibleForTesting static class CertSelectionFailureDialog { private final Activity mActivity; public CertSelectionFailureDialog(Activity activity) { mActivity = activity; } /** * Builds and shows the dialog. */ public void show() { final AlertDialog.Builder builder = new AlertDialog.Builder(mActivity, R.style.AlertDialogTheme); builder.setTitle(R.string.client_cert_unsupported_title) .setMessage(R.string.client_cert_unsupported_message) .setNegativeButton(R.string.close, new OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { // Do nothing } }); builder.show(); } } /** * Create a new asynchronous request to select a client certificate. * * @param nativePtr The native object responsible for this request. * @param window A WindowAndroid instance. * @param keyTypes The list of supported key exchange types. * @param encodedPrincipals The list of CA DistinguishedNames. * @param hostName The server host name is available (empty otherwise). * @param port The server port if available (0 otherwise). * @return true on success. * Note that nativeOnSystemRequestComplete will be called iff this method returns true. */ @CalledByNative private static boolean selectClientCertificate(final long nativePtr, final WindowAndroid window, final String[] keyTypes, byte[][] encodedPrincipals, final String hostName, final int port) { ThreadUtils.assertOnUiThread(); final Activity activity = window.getActivity().get(); if (activity == null) { Log.w(TAG, "Certificate request on GC'd activity."); return false; } // Build the list of principals from encoded versions. Principal[] principals = null; if (encodedPrincipals.length > 0) { principals = new X500Principal[encodedPrincipals.length]; try { for (int n = 0; n < encodedPrincipals.length; n++) { principals[n] = new X500Principal(encodedPrincipals[n]); } } catch (Exception e) { Log.w(TAG, "Exception while decoding issuers list: " + e); return false; } } KeyChainCertSelectionCallback callback = new KeyChainCertSelectionCallback(activity.getApplicationContext(), nativePtr); KeyChainCertSelectionWrapper keyChain = new KeyChainCertSelectionWrapper(activity, callback, keyTypes, principals, hostName, port, null); maybeShowCertSelection(keyChain, callback, new CertSelectionFailureDialog(activity)); // We've taken ownership of the native ssl request object. return true; } /** * Attempt to show the certificate selection dialog and shows the provided * CertSelectionFailureDialog if the platform's cert selection activity can't be found. */ @VisibleForTesting static void maybeShowCertSelection(KeyChainCertSelectionWrapper keyChain, KeyChainAliasCallback callback, CertSelectionFailureDialog failureDialog) { try { keyChain.choosePrivateKeyAlias(); } catch (ActivityNotFoundException e) { // This exception can be hit when a platform is missing the activity to select // a client certificate. It gets handled here to avoid a crash. // Complete the callback without selecting a certificate. callback.alias(null); // Show a dialog letting the user know that the system does not support // client certificate selection. failureDialog.show(); } } public static void notifyClientCertificatesChangedOnIOThread() { Log.d(TAG, "ClientCertificatesChanged!"); nativeNotifyClientCertificatesChangedOnIOThread(); } private static native void nativeNotifyClientCertificatesChangedOnIOThread(); // Called to pass request results to native side. private static native void nativeOnSystemRequestCompletion( long requestPtr, byte[][] certChain, PrivateKey privateKey); }