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