// Copyright (c) 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 java.security.cert.CertificateEncodingException; import java.security.cert.X509Certificate; import java.security.Principal; import java.security.PrivateKey; import javax.security.auth.x500.X500Principal; 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.ActivityStatus; import org.chromium.base.CalledByNative; import org.chromium.base.JNINamespace; import org.chromium.base.ThreadUtils; @JNINamespace("chrome::android") class SSLClientCertificateRequest extends AsyncTask implements KeyChainAliasCallback { static final String TAG = "SSLClientCertificateRequest"; // ClientCertRequest models an asynchronous client certificate request // on the Java side. Use selectClientCertificate() on the UI thread to // start/create a new request, this will launch a system activity // through KeyChain.choosePrivateKeyAlias() to let the user select // a client certificate. // // The selected certificate will be sent back as a string alias, // which is used to call KeyChain.getCertificateChain() and // KeyChain.getPrivateKey(). Unfortunately, these APIs are blocking, // thus can't be called from the UI thread. // // To solve this, start an AsyncTask when the alias is received. // it will retrieve the certificate chain and private key in the // background, then later send the result back to the UI thread. // private final int mNativePtr; private String mAlias; private byte[][] mEncodedChain; private PrivateKey mPrivateKey; private SSLClientCertificateRequest(int nativePtr) { mNativePtr = nativePtr; mAlias = null; mEncodedChain = null; mPrivateKey = null; } // KeyChainAliasCallback implementation @Override public void alias(String alias) { if (alias == null) { // No certificate was selected. onPostExecute(null); } else { mAlias = alias; // Launch background thread. execute(); } } @Override protected Void doInBackground(Void... params) { // Executed in a background thread, can call blocking APIs. X509Certificate[] chain = null; PrivateKey key = null; Context context = ActivityStatus.getActivity().getApplicationContext(); try { key = KeyChain.getPrivateKey(context, mAlias); chain = KeyChain.getCertificateChain(context, mAlias); } catch (KeyChainException e) { Log.w(TAG, "KeyChainException when looking for '" + mAlias + "' certificate"); return null; } catch (InterruptedException e) { Log.w(TAG, "InterruptedException when looking for '" + mAlias + "'certificate"); return null; } if (key == null || chain == null || chain.length == 0) { Log.w(TAG, "Empty client certificate chain?"); return null; } // Get the encoded certificate chain. byte[][] encoded_chain = new byte[chain.length][]; try { for (int i = 0; i < chain.length; ++i) { encoded_chain[i] = chain[i].getEncoded(); } } catch (CertificateEncodingException e) { Log.w(TAG, "Could not retrieve encoded certificate chain: " + e); return null; } mEncodedChain = encoded_chain; mPrivateKey = key; return null; } @Override protected void onPostExecute(Void result) { // Back to the UI thread. nativeOnSystemRequestCompletion(mNativePtr, mEncodedChain, mPrivateKey); } /** * Create a new asynchronous request to select a client certificate. * * @param nativePtr The native object responsible for this request. * @param keyTypes The list of supported key exchange types. * @param encodedPrincipals The list of CA DistinguishedNames. * @param host_name 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 static private boolean selectClientCertificate( int nativePtr, String[] keyTypes, byte[][] encodedPrincipals, String hostName, int port) { ThreadUtils.assertOnUiThread(); Activity activity = ActivityStatus.getActivity(); if (activity == null) { Log.w(TAG, "No active Chromium main 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) { // Bail on error. Log.w(TAG, "Exception while decoding issuers list: " + e); return false; } } // All good, create new request, add it to our list and launch // the certificate selection activity. SSLClientCertificateRequest request = new SSLClientCertificateRequest(nativePtr); KeyChain.choosePrivateKeyAlias( activity, request, keyTypes, principals, hostName, port, null); return true; } // Called to pass request results to native side. private static native void nativeOnSystemRequestCompletion( int requestPtr, byte[][] certChain, PrivateKey privateKey); }