KeyChainActivity.java revision 01b70dc5433d4a462df0a2d63163fcd47f52fa30
1/* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.keychain; 18 19import android.app.Activity; 20import android.app.ActivityManagerNative; 21import android.app.admin.IDevicePolicyManager; 22import android.app.AlertDialog; 23import android.app.Dialog; 24import android.app.PendingIntent; 25import android.content.Context; 26import android.content.DialogInterface; 27import android.content.Intent; 28import android.content.pm.PackageManager; 29import android.content.res.Resources; 30import android.net.Uri; 31import android.os.AsyncTask; 32import android.os.Bundle; 33import android.os.IBinder; 34import android.os.RemoteException; 35import android.os.ServiceManager; 36import android.security.Credentials; 37import android.security.IKeyChainAliasCallback; 38import android.security.KeyChain; 39import android.security.KeyStore; 40import android.util.Log; 41import android.view.LayoutInflater; 42import android.view.View; 43import android.view.ViewGroup; 44import android.widget.AdapterView; 45import android.widget.BaseAdapter; 46import android.widget.Button; 47import android.widget.ListView; 48import android.widget.RadioButton; 49import android.widget.TextView; 50import com.android.org.bouncycastle.asn1.x509.X509Name; 51import java.io.ByteArrayInputStream; 52import java.io.InputStream; 53import java.security.cert.CertificateException; 54import java.security.cert.CertificateFactory; 55import java.security.cert.X509Certificate; 56import java.util.ArrayList; 57import java.util.Arrays; 58import java.util.Collections; 59import java.util.concurrent.ExecutionException; 60import java.util.List; 61 62import javax.security.auth.x500.X500Principal; 63 64public class KeyChainActivity extends Activity { 65 private static final String TAG = "KeyChain"; 66 67 private static String KEY_STATE = "state"; 68 69 private static final int REQUEST_UNLOCK = 1; 70 71 private int mSenderUid; 72 73 private PendingIntent mSender; 74 75 private static enum State { INITIAL, UNLOCK_REQUESTED, UNLOCK_CANCELED }; 76 77 private State mState; 78 79 // beware that some of these KeyStore operations such as saw and 80 // get do file I/O in the remote keystore process and while they 81 // do not cause StrictMode violations, they logically should not 82 // be done on the UI thread. 83 private KeyStore mKeyStore = KeyStore.getInstance(); 84 85 @Override public void onCreate(Bundle savedState) { 86 super.onCreate(savedState); 87 if (savedState == null) { 88 mState = State.INITIAL; 89 } else { 90 mState = (State) savedState.getSerializable(KEY_STATE); 91 if (mState == null) { 92 mState = State.INITIAL; 93 } 94 } 95 } 96 97 @Override public void onResume() { 98 super.onResume(); 99 100 mSender = getIntent().getParcelableExtra(KeyChain.EXTRA_SENDER); 101 if (mSender == null) { 102 // if no sender, bail, we need to identify the app to the user securely. 103 finish(null); 104 return; 105 } 106 try { 107 mSenderUid = getPackageManager().getPackageInfo( 108 mSender.getIntentSender().getTargetPackage(), 0).applicationInfo.uid; 109 } catch (PackageManager.NameNotFoundException e) { 110 // if unable to find the sender package info bail, 111 // we need to identify the app to the user securely. 112 finish(null); 113 return; 114 } 115 116 // see if KeyStore has been unlocked, if not start activity to do so 117 switch (mState) { 118 case INITIAL: 119 if (!mKeyStore.isUnlocked()) { 120 mState = State.UNLOCK_REQUESTED; 121 this.startActivityForResult(new Intent(Credentials.UNLOCK_ACTION), 122 REQUEST_UNLOCK); 123 // Note that Credentials.unlock will start an 124 // Activity and we will be paused but then resumed 125 // when the unlock Activity completes and our 126 // onActivityResult is called with REQUEST_UNLOCK 127 return; 128 } 129 chooseCertificate(); 130 return; 131 case UNLOCK_REQUESTED: 132 // we've already asked, but have not heard back, probably just rotated. 133 // wait to hear back via onActivityResult 134 return; 135 case UNLOCK_CANCELED: 136 // User wanted to cancel the request, so exit. 137 mState = State.INITIAL; 138 finish(null); 139 return; 140 default: 141 throw new AssertionError(); 142 } 143 } 144 145 private void chooseCertificate() { 146 // Start loading the set of certs to choose from now- if device policy doesn't return an 147 // alias, having aliases loading already will save some time waiting for UI to start. 148 final AliasLoader loader = new AliasLoader(); 149 loader.execute(); 150 151 final IKeyChainAliasCallback.Stub callback = new IKeyChainAliasCallback.Stub() { 152 @Override public void alias(String alias) { 153 // Use policy-suggested alias if provided 154 if (alias != null) { 155 finish(alias); 156 return; 157 } 158 159 // No suggested alias - instead finish loading and show UI to pick one 160 final CertificateAdapter certAdapter; 161 try { 162 certAdapter = loader.get(); 163 } catch (InterruptedException | ExecutionException e) { 164 Log.e(TAG, "Loading certificate aliases interrupted", e); 165 finish(null); 166 return; 167 } 168 runOnUiThread(new Runnable() { 169 @Override public void run() { 170 displayCertChooserDialog(certAdapter); 171 } 172 }); 173 } 174 }; 175 176 // Give a profile or device owner the chance to intercept the request, if a private key 177 // access listener is registered with the DevicePolicyManagerService. 178 IDevicePolicyManager devicePolicyManager = IDevicePolicyManager.Stub.asInterface( 179 ServiceManager.getService(Context.DEVICE_POLICY_SERVICE)); 180 181 Uri uri = getIntent().getParcelableExtra(KeyChain.EXTRA_URI); 182 String alias = getIntent().getStringExtra(KeyChain.EXTRA_ALIAS); 183 try { 184 devicePolicyManager.choosePrivateKeyAlias(mSenderUid, uri, alias, callback); 185 } catch (RemoteException e) { 186 Log.e(TAG, "Unable to request alias from DevicePolicyManager", e); 187 // Proceed without a suggested alias. 188 try { 189 callback.alias(null); 190 } catch (RemoteException shouldNeverHappen) { 191 finish(null); 192 } 193 } 194 } 195 196 private class AliasLoader extends AsyncTask<Void, Void, CertificateAdapter> { 197 @Override protected CertificateAdapter doInBackground(Void... params) { 198 String[] aliasArray = mKeyStore.list(Credentials.USER_PRIVATE_KEY); 199 List<String> aliasList = ((aliasArray == null) 200 ? Collections.<String>emptyList() 201 : Arrays.asList(aliasArray)); 202 Collections.sort(aliasList); 203 return new CertificateAdapter(aliasList); 204 } 205 } 206 207 private void displayCertChooserDialog(final CertificateAdapter adapter) { 208 AlertDialog.Builder builder = new AlertDialog.Builder(this); 209 210 TextView contextView = (TextView) View.inflate(this, R.layout.cert_chooser_header, null); 211 View footer = View.inflate(this, R.layout.cert_chooser_footer, null); 212 213 final ListView lv = (ListView) View.inflate(this, R.layout.cert_chooser, null); 214 lv.addHeaderView(contextView, null, false); 215 lv.addFooterView(footer, null, false); 216 lv.setChoiceMode(ListView.CHOICE_MODE_SINGLE); 217 lv.setAdapter(adapter); 218 builder.setView(lv); 219 220 lv.setOnItemClickListener(new AdapterView.OnItemClickListener() { 221 222 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 223 lv.setItemChecked(position, true); 224 adapter.notifyDataSetChanged(); 225 } 226 }); 227 228 boolean empty = adapter.mAliases.isEmpty(); 229 int negativeLabel = empty ? android.R.string.cancel : R.string.deny_button; 230 builder.setNegativeButton(negativeLabel, new DialogInterface.OnClickListener() { 231 @Override public void onClick(DialogInterface dialog, int id) { 232 dialog.cancel(); // will cause OnDismissListener to be called 233 } 234 }); 235 236 String title; 237 Resources res = getResources(); 238 if (empty) { 239 title = res.getString(R.string.title_no_certs); 240 } else { 241 title = res.getString(R.string.title_select_cert); 242 String alias = getIntent().getStringExtra(KeyChain.EXTRA_ALIAS); 243 if (alias != null) { 244 // if alias was requested, set it if found 245 int adapterPosition = adapter.mAliases.indexOf(alias); 246 if (adapterPosition != -1) { 247 int listViewPosition = adapterPosition+1; 248 lv.setItemChecked(listViewPosition, true); 249 } 250 } else if (adapter.mAliases.size() == 1) { 251 // if only one choice, preselect it 252 int adapterPosition = 0; 253 int listViewPosition = adapterPosition+1; 254 lv.setItemChecked(listViewPosition, true); 255 } 256 257 builder.setPositiveButton(R.string.allow_button, new DialogInterface.OnClickListener() { 258 @Override public void onClick(DialogInterface dialog, int id) { 259 int listViewPosition = lv.getCheckedItemPosition(); 260 int adapterPosition = listViewPosition-1; 261 String alias = ((adapterPosition >= 0) 262 ? adapter.getItem(adapterPosition) 263 : null); 264 finish(alias); 265 } 266 }); 267 } 268 builder.setTitle(title); 269 final Dialog dialog = builder.create(); 270 271 272 // getTargetPackage guarantees that the returned string is 273 // supplied by the system, so that an application can not 274 // spoof its package. 275 String pkg = mSender.getIntentSender().getTargetPackage(); 276 PackageManager pm = getPackageManager(); 277 CharSequence applicationLabel; 278 try { 279 applicationLabel = pm.getApplicationLabel(pm.getApplicationInfo(pkg, 0)).toString(); 280 } catch (PackageManager.NameNotFoundException e) { 281 applicationLabel = pkg; 282 } 283 String appMessage = String.format(res.getString(R.string.requesting_application), 284 applicationLabel); 285 String contextMessage = appMessage; 286 Uri uri = getIntent().getParcelableExtra(KeyChain.EXTRA_URI); 287 if (uri != null) { 288 String hostMessage = String.format(res.getString(R.string.requesting_server), 289 uri.getAuthority()); 290 if (contextMessage == null) { 291 contextMessage = hostMessage; 292 } else { 293 contextMessage += " " + hostMessage; 294 } 295 } 296 contextView.setText(contextMessage); 297 298 String installMessage = String.format(res.getString(R.string.install_new_cert_message), 299 Credentials.EXTENSION_PFX, Credentials.EXTENSION_P12); 300 TextView installText = (TextView) footer.findViewById(R.id.cert_chooser_install_message); 301 installText.setText(installMessage); 302 303 Button installButton = (Button) footer.findViewById(R.id.cert_chooser_install_button); 304 installButton.setOnClickListener(new View.OnClickListener() { 305 @Override public void onClick(View v) { 306 // remove dialog so that we will recreate with 307 // possibly new content after install returns 308 dialog.dismiss(); 309 Credentials.getInstance().install(KeyChainActivity.this); 310 } 311 }); 312 313 dialog.setOnCancelListener(new DialogInterface.OnCancelListener() { 314 @Override public void onCancel(DialogInterface dialog) { 315 finish(null); 316 } 317 }); 318 dialog.show(); 319 } 320 321 private class CertificateAdapter extends BaseAdapter { 322 private final List<String> mAliases; 323 private final List<String> mSubjects = new ArrayList<String>(); 324 private CertificateAdapter(List<String> aliases) { 325 mAliases = aliases; 326 mSubjects.addAll(Collections.nCopies(aliases.size(), (String) null)); 327 } 328 @Override public int getCount() { 329 return mAliases.size(); 330 } 331 @Override public String getItem(int adapterPosition) { 332 return mAliases.get(adapterPosition); 333 } 334 @Override public long getItemId(int adapterPosition) { 335 return adapterPosition; 336 } 337 @Override public View getView(final int adapterPosition, View view, ViewGroup parent) { 338 ViewHolder holder; 339 if (view == null) { 340 LayoutInflater inflater = LayoutInflater.from(KeyChainActivity.this); 341 view = inflater.inflate(R.layout.cert_item, parent, false); 342 holder = new ViewHolder(); 343 holder.mAliasTextView = (TextView) view.findViewById(R.id.cert_item_alias); 344 holder.mSubjectTextView = (TextView) view.findViewById(R.id.cert_item_subject); 345 holder.mRadioButton = (RadioButton) view.findViewById(R.id.cert_item_selected); 346 view.setTag(holder); 347 } else { 348 holder = (ViewHolder) view.getTag(); 349 } 350 351 String alias = mAliases.get(adapterPosition); 352 353 holder.mAliasTextView.setText(alias); 354 355 String subject = mSubjects.get(adapterPosition); 356 if (subject == null) { 357 new CertLoader(adapterPosition, holder.mSubjectTextView).execute(); 358 } else { 359 holder.mSubjectTextView.setText(subject); 360 } 361 362 ListView lv = (ListView)parent; 363 int listViewCheckedItemPosition = lv.getCheckedItemPosition(); 364 int adapterCheckedItemPosition = listViewCheckedItemPosition-1; 365 holder.mRadioButton.setChecked(adapterPosition == adapterCheckedItemPosition); 366 return view; 367 } 368 369 private class CertLoader extends AsyncTask<Void, Void, String> { 370 private final int mAdapterPosition; 371 private final TextView mSubjectView; 372 private CertLoader(int adapterPosition, TextView subjectView) { 373 mAdapterPosition = adapterPosition; 374 mSubjectView = subjectView; 375 } 376 @Override protected String doInBackground(Void... params) { 377 String alias = mAliases.get(mAdapterPosition); 378 byte[] bytes = mKeyStore.get(Credentials.USER_CERTIFICATE + alias); 379 if (bytes == null) { 380 return null; 381 } 382 InputStream in = new ByteArrayInputStream(bytes); 383 X509Certificate cert; 384 try { 385 CertificateFactory cf = CertificateFactory.getInstance("X.509"); 386 cert = (X509Certificate)cf.generateCertificate(in); 387 } catch (CertificateException ignored) { 388 return null; 389 } 390 // bouncycastle can handle the emailAddress OID of 1.2.840.113549.1.9.1 391 X500Principal subjectPrincipal = cert.getSubjectX500Principal(); 392 X509Name subjectName = X509Name.getInstance(subjectPrincipal.getEncoded()); 393 String subjectString = subjectName.toString(true, X509Name.DefaultSymbols); 394 return subjectString; 395 } 396 @Override protected void onPostExecute(String subjectString) { 397 mSubjects.set(mAdapterPosition, subjectString); 398 mSubjectView.setText(subjectString); 399 } 400 } 401 } 402 403 private static class ViewHolder { 404 TextView mAliasTextView; 405 TextView mSubjectTextView; 406 RadioButton mRadioButton; 407 } 408 409 @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { 410 switch (requestCode) { 411 case REQUEST_UNLOCK: 412 if (mKeyStore.isUnlocked()) { 413 mState = State.INITIAL; 414 chooseCertificate(); 415 } else { 416 // user must have canceled unlock, give up 417 mState = State.UNLOCK_CANCELED; 418 } 419 return; 420 default: 421 throw new AssertionError(); 422 } 423 } 424 425 private void finish(String alias) { 426 if (alias == null) { 427 setResult(RESULT_CANCELED); 428 } else { 429 Intent result = new Intent(); 430 result.putExtra(Intent.EXTRA_TEXT, alias); 431 setResult(RESULT_OK, result); 432 } 433 IKeyChainAliasCallback keyChainAliasResponse 434 = IKeyChainAliasCallback.Stub.asInterface( 435 getIntent().getIBinderExtra(KeyChain.EXTRA_RESPONSE)); 436 if (keyChainAliasResponse != null) { 437 new ResponseSender(keyChainAliasResponse, alias).execute(); 438 return; 439 } 440 finish(); 441 } 442 443 private class ResponseSender extends AsyncTask<Void, Void, Void> { 444 private IKeyChainAliasCallback mKeyChainAliasResponse; 445 private String mAlias; 446 private ResponseSender(IKeyChainAliasCallback keyChainAliasResponse, String alias) { 447 mKeyChainAliasResponse = keyChainAliasResponse; 448 mAlias = alias; 449 } 450 @Override protected Void doInBackground(Void... unused) { 451 try { 452 if (mAlias != null) { 453 KeyChain.KeyChainConnection connection = KeyChain.bind(KeyChainActivity.this); 454 try { 455 connection.getService().setGrant(mSenderUid, mAlias, true); 456 } finally { 457 connection.close(); 458 } 459 } 460 mKeyChainAliasResponse.alias(mAlias); 461 } catch (InterruptedException ignored) { 462 Thread.currentThread().interrupt(); 463 Log.d(TAG, "interrupted while granting access", ignored); 464 } catch (Exception ignored) { 465 // don't just catch RemoteException, caller could 466 // throw back a RuntimeException across processes 467 // which we should protect against. 468 Log.e(TAG, "error while granting access", ignored); 469 } 470 return null; 471 } 472 @Override protected void onPostExecute(Void unused) { 473 finish(); 474 } 475 } 476 477 @Override public void onBackPressed() { 478 finish(null); 479 } 480 481 @Override protected void onSaveInstanceState(Bundle savedState) { 482 super.onSaveInstanceState(savedState); 483 if (mState != State.INITIAL) { 484 savedState.putSerializable(KEY_STATE, mState); 485 } 486 } 487} 488