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 17 package com.android.keychain; 18 19 import android.app.Activity; 20 import android.app.AlertDialog; 21 import android.app.Dialog; 22 import android.app.PendingIntent; 23 import android.content.DialogInterface; 24 import android.content.Intent; 25 import android.content.pm.PackageManager; 26 import android.content.res.Resources; 27 import android.os.AsyncTask; 28 import android.os.Bundle; 29 import android.security.Credentials; 30 import android.security.IKeyChainAliasCallback; 31 import android.security.KeyChain; 32 import android.security.KeyStore; 33 import android.util.Log; 34 import android.view.LayoutInflater; 35 import android.view.View; 36 import android.view.ViewGroup; 37 import android.widget.BaseAdapter; 38 import android.widget.Button; 39 import android.widget.ListView; 40 import android.widget.RadioButton; 41 import android.widget.TextView; 42 import com.android.org.bouncycastle.asn1.x509.X509Name; 43 import java.io.ByteArrayInputStream; 44 import java.io.InputStream; 45 import java.security.cert.CertificateException; 46 import java.security.cert.CertificateFactory; 47 import java.security.cert.X509Certificate; 48 import java.util.ArrayList; 49 import java.util.Arrays; 50 import java.util.Collections; 51 import java.util.List; 52 53 import javax.security.auth.x500.X500Principal; 54 55 public class KeyChainActivity extends Activity { 56 private static final String TAG = "KeyChain"; 57 58 private static String KEY_STATE = "state"; 59 60 private static final int REQUEST_UNLOCK = 1; 61 62 private int mSenderUid; 63 64 private PendingIntent mSender; 65 66 private static enum State { INITIAL, UNLOCK_REQUESTED }; 67 68 private State mState; 69 70 // beware that some of these KeyStore operations such as saw and 71 // get do file I/O in the remote keystore process and while they 72 // do not cause StrictMode violations, they logically should not 73 // be done on the UI thread. 74 private KeyStore mKeyStore = KeyStore.getInstance(); 75 76 // the KeyStore.state operation is safe to do on the UI thread, it 77 // does not do a file operation. 78 private boolean isKeyStoreUnlocked() { 79 return mKeyStore.state() == KeyStore.State.UNLOCKED; 80 } 81 82 @Override public void onCreate(Bundle savedState) { 83 super.onCreate(savedState); 84 if (savedState == null) { 85 mState = State.INITIAL; 86 } else { 87 mState = (State) savedState.getSerializable(KEY_STATE); 88 if (mState == null) { 89 mState = State.INITIAL; 90 } 91 } 92 } 93 94 @Override public void onResume() { 95 super.onResume(); 96 97 mSender = getIntent().getParcelableExtra(KeyChain.EXTRA_SENDER); 98 if (mSender == null) { 99 // if no sender, bail, we need to identify the app to the user securely. 100 finish(null); 101 return; 102 } 103 try { 104 mSenderUid = getPackageManager().getPackageInfo( 105 mSender.getIntentSender().getTargetPackage(), 0).applicationInfo.uid; 106 } catch (PackageManager.NameNotFoundException e) { 107 // if unable to find the sender package info bail, 108 // we need to identify the app to the user securely. 109 finish(null); 110 return; 111 } 112 113 // see if KeyStore has been unlocked, if not start activity to do so 114 switch (mState) { 115 case INITIAL: 116 if (!isKeyStoreUnlocked()) { 117 mState = State.UNLOCK_REQUESTED; 118 this.startActivityForResult(new Intent(Credentials.UNLOCK_ACTION), 119 REQUEST_UNLOCK); 120 // Note that Credentials.unlock will start an 121 // Activity and we will be paused but then resumed 122 // when the unlock Activity completes and our 123 // onActivityResult is called with REQUEST_UNLOCK 124 return; 125 } 126 showCertChooserDialog(); 127 return; 128 case UNLOCK_REQUESTED: 129 // we've already asked, but have not heard back, probably just rotated. 130 // wait to hear back via onActivityResult 131 return; 132 default: 133 throw new AssertionError(); 134 } 135 } 136 137 private void showCertChooserDialog() { 138 new AliasLoader().execute(); 139 } 140 141 private class AliasLoader extends AsyncTask<Void, Void, CertificateAdapter> { 142 @Override protected CertificateAdapter doInBackground(Void... params) { 143 String[] aliasArray = mKeyStore.saw(Credentials.USER_PRIVATE_KEY); 144 List<String> aliasList = ((aliasArray == null) 145 ? Collections.<String>emptyList() 146 : Arrays.asList(aliasArray)); 147 Collections.sort(aliasList); 148 return new CertificateAdapter(aliasList); 149 } 150 @Override protected void onPostExecute(CertificateAdapter adapter) { 151 displayCertChooserDialog(adapter); 152 } 153 } 154 155 private void displayCertChooserDialog(final CertificateAdapter adapter) { 156 AlertDialog.Builder builder = new AlertDialog.Builder(this); 157 158 TextView contextView = (TextView) View.inflate(this, R.layout.cert_chooser_header, null); 159 View footer = View.inflate(this, R.layout.cert_chooser_footer, null); 160 161 final ListView lv = (ListView) View.inflate(this, R.layout.cert_chooser, null); 162 lv.addHeaderView(contextView, null, false); 163 lv.addFooterView(footer, null, false); 164 lv.setAdapter(adapter); 165 builder.setView(lv); 166 167 boolean empty = adapter.mAliases.isEmpty(); 168 int negativeLabel = empty ? android.R.string.cancel : R.string.deny_button; 169 builder.setNegativeButton(negativeLabel, new DialogInterface.OnClickListener() { 170 @Override public void onClick(DialogInterface dialog, int id) { 171 dialog.cancel(); // will cause OnDismissListener to be called 172 } 173 }); 174 175 String title; 176 Resources res = getResources(); 177 if (empty) { 178 title = res.getString(R.string.title_no_certs); 179 } else { 180 title = res.getString(R.string.title_select_cert); 181 String alias = getIntent().getStringExtra(KeyChain.EXTRA_ALIAS); 182 if (alias != null) { 183 // if alias was requested, set it if found 184 int adapterPosition = adapter.mAliases.indexOf(alias); 185 if (adapterPosition != -1) { 186 int listViewPosition = adapterPosition+1; 187 lv.setItemChecked(listViewPosition, true); 188 } 189 } else if (adapter.mAliases.size() == 1) { 190 // if only one choice, preselect it 191 int adapterPosition = 0; 192 int listViewPosition = adapterPosition+1; 193 lv.setItemChecked(listViewPosition, true); 194 } 195 196 builder.setPositiveButton(R.string.allow_button, new DialogInterface.OnClickListener() { 197 @Override public void onClick(DialogInterface dialog, int id) { 198 int listViewPosition = lv.getCheckedItemPosition(); 199 int adapterPosition = listViewPosition-1; 200 String alias = ((adapterPosition >= 0) 201 ? adapter.getItem(adapterPosition) 202 : null); 203 finish(alias); 204 } 205 }); 206 } 207 builder.setTitle(title); 208 final Dialog dialog = builder.create(); 209 210 211 // getTargetPackage guarantees that the returned string is 212 // supplied by the system, so that an application can not 213 // spoof its package. 214 String pkg = mSender.getIntentSender().getTargetPackage(); 215 PackageManager pm = getPackageManager(); 216 CharSequence applicationLabel; 217 try { 218 applicationLabel = pm.getApplicationLabel(pm.getApplicationInfo(pkg, 0)).toString(); 219 } catch (PackageManager.NameNotFoundException e) { 220 applicationLabel = pkg; 221 } 222 String appMessage = String.format(res.getString(R.string.requesting_application), 223 applicationLabel); 224 225 String contextMessage = appMessage; 226 String host = getIntent().getStringExtra(KeyChain.EXTRA_HOST); 227 if (host != null) { 228 String hostString = host; 229 int port = getIntent().getIntExtra(KeyChain.EXTRA_PORT, -1); 230 if (port != -1) { 231 hostString += ":" + port; 232 } 233 String hostMessage = String.format(res.getString(R.string.requesting_server), 234 hostString); 235 if (contextMessage == null) { 236 contextMessage = hostMessage; 237 } else { 238 contextMessage += " " + hostMessage; 239 } 240 } 241 contextView.setText(contextMessage); 242 243 String installMessage = String.format(res.getString(R.string.install_new_cert_message), 244 Credentials.EXTENSION_PFX, Credentials.EXTENSION_P12); 245 TextView installText = (TextView) footer.findViewById(R.id.cert_chooser_install_message); 246 installText.setText(installMessage); 247 248 Button installButton = (Button) footer.findViewById(R.id.cert_chooser_install_button); 249 installButton.setOnClickListener(new View.OnClickListener() { 250 @Override public void onClick(View v) { 251 // remove dialog so that we will recreate with 252 // possibly new content after install returns 253 dialog.dismiss(); 254 Credentials.getInstance().install(KeyChainActivity.this); 255 } 256 }); 257 258 dialog.setOnCancelListener(new DialogInterface.OnCancelListener() { 259 @Override public void onCancel(DialogInterface dialog) { 260 finish(null); 261 } 262 }); 263 dialog.show(); 264 } 265 266 private class CertificateAdapter extends BaseAdapter { 267 private final List<String> mAliases; 268 private final List<String> mSubjects = new ArrayList<String>(); 269 private CertificateAdapter(List<String> aliases) { 270 mAliases = aliases; 271 mSubjects.addAll(Collections.nCopies(aliases.size(), (String) null)); 272 } 273 @Override public int getCount() { 274 return mAliases.size(); 275 } 276 @Override public String getItem(int adapterPosition) { 277 return mAliases.get(adapterPosition); 278 } 279 @Override public long getItemId(int adapterPosition) { 280 return adapterPosition; 281 } 282 @Override public View getView(final int adapterPosition, View view, ViewGroup parent) { 283 ViewHolder holder; 284 if (view == null) { 285 LayoutInflater inflater = LayoutInflater.from(KeyChainActivity.this); 286 view = inflater.inflate(R.layout.cert_item, parent, false); 287 holder = new ViewHolder(); 288 holder.mAliasTextView = (TextView) view.findViewById(R.id.cert_item_alias); 289 holder.mSubjectTextView = (TextView) view.findViewById(R.id.cert_item_subject); 290 holder.mRadioButton = (RadioButton) view.findViewById(R.id.cert_item_selected); 291 view.setTag(holder); 292 } else { 293 holder = (ViewHolder) view.getTag(); 294 } 295 296 String alias = mAliases.get(adapterPosition); 297 298 holder.mAliasTextView.setText(alias); 299 300 String subject = mSubjects.get(adapterPosition); 301 if (subject == null) { 302 new CertLoader(adapterPosition, holder.mSubjectTextView).execute(); 303 } else { 304 holder.mSubjectTextView.setText(subject); 305 } 306 307 ListView lv = (ListView)parent; 308 int listViewCheckedItemPosition = lv.getCheckedItemPosition(); 309 int adapterCheckedItemPosition = listViewCheckedItemPosition-1; 310 holder.mRadioButton.setChecked(adapterPosition == adapterCheckedItemPosition); 311 return view; 312 } 313 314 private class CertLoader extends AsyncTask<Void, Void, String> { 315 private final int mAdapterPosition; 316 private final TextView mSubjectView; 317 private CertLoader(int adapterPosition, TextView subjectView) { 318 mAdapterPosition = adapterPosition; 319 mSubjectView = subjectView; 320 } 321 @Override protected String doInBackground(Void... params) { 322 String alias = mAliases.get(mAdapterPosition); 323 byte[] bytes = mKeyStore.get(Credentials.USER_CERTIFICATE + alias); 324 if (bytes == null) { 325 return null; 326 } 327 InputStream in = new ByteArrayInputStream(bytes); 328 X509Certificate cert; 329 try { 330 CertificateFactory cf = CertificateFactory.getInstance("X.509"); 331 cert = (X509Certificate)cf.generateCertificate(in); 332 } catch (CertificateException ignored) { 333 return null; 334 } 335 // bouncycastle can handle the emailAddress OID of 1.2.840.113549.1.9.1 336 X500Principal subjectPrincipal = cert.getSubjectX500Principal(); 337 X509Name subjectName = X509Name.getInstance(subjectPrincipal.getEncoded()); 338 String subjectString = subjectName.toString(true, X509Name.DefaultSymbols); 339 return subjectString; 340 } 341 @Override protected void onPostExecute(String subjectString) { 342 mSubjects.set(mAdapterPosition, subjectString); 343 mSubjectView.setText(subjectString); 344 } 345 } 346 } 347 348 private static class ViewHolder { 349 TextView mAliasTextView; 350 TextView mSubjectTextView; 351 RadioButton mRadioButton; 352 } 353 354 @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { 355 switch (requestCode) { 356 case REQUEST_UNLOCK: 357 if (isKeyStoreUnlocked()) { 358 showCertChooserDialog(); 359 } else { 360 // user must have canceled unlock, give up 361 finish(null); 362 } 363 return; 364 default: 365 throw new AssertionError(); 366 } 367 } 368 369 private void finish(String alias) { 370 if (alias == null) { 371 setResult(RESULT_CANCELED); 372 } else { 373 Intent result = new Intent(); 374 result.putExtra(Intent.EXTRA_TEXT, alias); 375 setResult(RESULT_OK, result); 376 } 377 IKeyChainAliasCallback keyChainAliasResponse 378 = IKeyChainAliasCallback.Stub.asInterface( 379 getIntent().getIBinderExtra(KeyChain.EXTRA_RESPONSE)); 380 if (keyChainAliasResponse != null) { 381 new ResponseSender(keyChainAliasResponse, alias).execute(); 382 return; 383 } 384 finish(); 385 } 386 387 private class ResponseSender extends AsyncTask<Void, Void, Void> { 388 private IKeyChainAliasCallback mKeyChainAliasResponse; 389 private String mAlias; 390 private ResponseSender(IKeyChainAliasCallback keyChainAliasResponse, String alias) { 391 mKeyChainAliasResponse = keyChainAliasResponse; 392 mAlias = alias; 393 } 394 @Override protected Void doInBackground(Void... unused) { 395 try { 396 if (mAlias != null) { 397 KeyChain.KeyChainConnection connection = KeyChain.bind(KeyChainActivity.this); 398 try { 399 connection.getService().setGrant(mSenderUid, mAlias, true); 400 } finally { 401 connection.close(); 402 } 403 } 404 mKeyChainAliasResponse.alias(mAlias); 405 } catch (InterruptedException ignored) { 406 Thread.currentThread().interrupt(); 407 Log.d(TAG, "interrupted while granting access", ignored); 408 } catch (Exception ignored) { 409 // don't just catch RemoteException, caller could 410 // throw back a RuntimeException across processes 411 // which we should protect against. 412 Log.e(TAG, "error while granting access", ignored); 413 } 414 return null; 415 } 416 @Override protected void onPostExecute(Void unused) { 417 finish(); 418 } 419 } 420 421 @Override public void onBackPressed() { 422 finish(null); 423 } 424 425 @Override protected void onSaveInstanceState(Bundle savedState) { 426 super.onSaveInstanceState(savedState); 427 if (mState != State.INITIAL) { 428 savedState.putSerializable(KEY_STATE, mState); 429 } 430 } 431 } 432