| // 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.Context; |
| import android.os.AsyncTask; |
| import android.security.KeyChain; |
| import android.security.KeyChainAliasCallback; |
| import android.security.KeyChainException; |
| import android.util.Log; |
| |
| import org.chromium.base.CalledByNative; |
| import org.chromium.base.JNINamespace; |
| import org.chromium.base.ThreadUtils; |
| import org.chromium.net.AndroidPrivateKey; |
| import org.chromium.net.DefaultAndroidKeyStore; |
| import org.chromium.ui.base.WindowAndroid; |
| |
| import java.security.Principal; |
| 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"; |
| |
| private static final DefaultAndroidKeyStore sLocalKeyStore = |
| new DefaultAndroidKeyStore(); |
| |
| /** |
| * Common implementation for anynchronous task of handling the certificate request. This |
| * AsyncTask uses the abstract methods to retrieve the authentication material from a |
| * generalized 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. |
| */ |
| abstract static class CertAsyncTask 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 AndroidPrivateKey mAndroidPrivateKey; |
| |
| // Pointer to the native certificate request needed to return the results. |
| private final long mNativePtr; |
| |
| CertAsyncTask(long nativePtr) { |
| mNativePtr = nativePtr; |
| } |
| |
| // These overriden methods will be used to access the key store. |
| abstract String getAlias(); |
| abstract AndroidPrivateKey getPrivateKey(String alias); |
| abstract X509Certificate[] getCertificateChain(String alias); |
| |
| @Override |
| protected Void doInBackground(Void... params) { |
| String alias = getAlias(); |
| if (alias == null) return null; |
| |
| AndroidPrivateKey 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; |
| mAndroidPrivateKey = key; |
| return null; |
| } |
| |
| @Override |
| protected void onPostExecute(Void result) { |
| ThreadUtils.assertOnUiThread(); |
| nativeOnSystemRequestCompletion(mNativePtr, mEncodedChain, mAndroidPrivateKey); |
| } |
| } |
| |
| /** Implementation of CertAsyncTask for the system KeyChain API. */ |
| private static class CertAsyncTaskKeyChain extends CertAsyncTask { |
| final Context mContext; |
| final String mAlias; |
| |
| CertAsyncTaskKeyChain(Context context, long nativePtr, String alias) { |
| super(nativePtr); |
| mContext = context; |
| assert alias != null; |
| mAlias = alias; |
| } |
| |
| @Override |
| String getAlias() { |
| return mAlias; |
| } |
| |
| @Override |
| AndroidPrivateKey getPrivateKey(String alias) { |
| try { |
| return sLocalKeyStore.createKey(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; |
| } |
| } |
| |
| @Override |
| 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; |
| } |
| } |
| } |
| |
| /** Implementation of CertAsyncTask for use with a PKCS11-backed KeyStore. */ |
| private static class CertAsyncTaskPKCS11 extends CertAsyncTask { |
| private final PKCS11AuthenticationManager mPKCS11AuthManager; |
| private final String mHostName; |
| private final int mPort; |
| |
| CertAsyncTaskPKCS11(long nativePtr, String hostName, int port, |
| PKCS11AuthenticationManager pkcs11CardAuthManager) { |
| super(nativePtr); |
| mHostName = hostName; |
| mPort = port; |
| mPKCS11AuthManager = pkcs11CardAuthManager; |
| } |
| |
| @Override |
| String getAlias() { |
| return mPKCS11AuthManager.getClientCertificateAlias(mHostName, mPort); |
| } |
| |
| @Override |
| AndroidPrivateKey getPrivateKey(String alias) { |
| return mPKCS11AuthManager.getPrivateKey(alias); |
| } |
| |
| @Override |
| X509Certificate[] getCertificateChain(String alias) { |
| return mPKCS11AuthManager.getCertificateChain(alias); |
| } |
| } |
| |
| /** |
| * 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(); |
| } |
| } |
| }); |
| } |
| } |
| |
| /** |
| * 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; |
| } |
| } |
| |
| final Principal[] principalsForCallback = principals; |
| // Certificate for client authentication can be obtained either from the system store of |
| // from a smart card (if available). |
| Runnable useSystemStore = new Runnable() { |
| @Override |
| public void run() { |
| KeyChainCertSelectionCallback callback = |
| new KeyChainCertSelectionCallback(activity.getApplicationContext(), |
| nativePtr); |
| KeyChain.choosePrivateKeyAlias(activity, callback, keyTypes, principalsForCallback, |
| hostName, port, null); |
| } |
| }; |
| |
| final Context appContext = activity.getApplicationContext(); |
| final PKCS11AuthenticationManager smartCardAuthManager = |
| ((ChromiumApplication) appContext).getPKCS11AuthenticationManager(); |
| if (smartCardAuthManager.isPKCS11AuthEnabled()) { |
| // Smart card support is available, prompt the user whether to use it or Android system |
| // store. |
| Runnable useSmartCard = new Runnable() { |
| @Override |
| public void run() { |
| new CertAsyncTaskPKCS11(nativePtr, hostName, port, |
| smartCardAuthManager).execute(); |
| } |
| }; |
| Runnable cancelRunnable = new Runnable() { |
| @Override |
| public void run() { |
| // We took ownership of the request, need to delete it. |
| nativeOnSystemRequestCompletion(nativePtr, null, null); |
| } |
| }; |
| |
| KeyStoreSelectionDialog selectionDialog = new KeyStoreSelectionDialog( |
| useSystemStore, useSmartCard, cancelRunnable); |
| selectionDialog.show(activity.getFragmentManager(), null); |
| } else { |
| // Smart card support is not available, use the system store unconditionally. |
| useSystemStore.run(); |
| } |
| |
| // We've taken ownership of the native ssl request object. |
| return true; |
| } |
| |
| 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, AndroidPrivateKey androidKey); |
| } |