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, UNLOCK_CANCELED }; 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 onCreate(Bundle savedState)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 onResume()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 case UNLOCK_CANCELED: 128 // User wanted to cancel the request, so exit. 129 mState = State.INITIAL; 130 finish(null); 131 return; 132 default: 133 throw new AssertionError(); 134 } 135 } 136 showCertChooserDialog()137 private void showCertChooserDialog() { 138 new AliasLoader().execute(); 139 } 140 141 private class AliasLoader extends AsyncTask<Void, Void, CertificateAdapter> { doInBackground(Void... params)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 } onPostExecute(CertificateAdapter adapter)150 @Override protected void onPostExecute(CertificateAdapter adapter) { 151 displayCertChooserDialog(adapter); 152 } 153 } 154 displayCertChooserDialog(final CertificateAdapter adapter)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 lv.setOnItemClickListener(new AdapterView.OnItemClickListener() { 168 169 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 170 lv.setItemChecked(position, true); 171 } 172 }); 173 174 boolean empty = adapter.mAliases.isEmpty(); 175 int negativeLabel = empty ? android.R.string.cancel : R.string.deny_button; 176 builder.setNegativeButton(negativeLabel, new DialogInterface.OnClickListener() { 177 @Override public void onClick(DialogInterface dialog, int id) { 178 dialog.cancel(); // will cause OnDismissListener to be called 179 } 180 }); 181 182 String title; 183 Resources res = getResources(); 184 if (empty) { 185 title = res.getString(R.string.title_no_certs); 186 } else { 187 title = res.getString(R.string.title_select_cert); 188 String alias = getIntent().getStringExtra(KeyChain.EXTRA_ALIAS); 189 if (alias != null) { 190 // if alias was requested, set it if found 191 int adapterPosition = adapter.mAliases.indexOf(alias); 192 if (adapterPosition != -1) { 193 int listViewPosition = adapterPosition+1; 194 lv.setItemChecked(listViewPosition, true); 195 } 196 } else if (adapter.mAliases.size() == 1) { 197 // if only one choice, preselect it 198 int adapterPosition = 0; 199 int listViewPosition = adapterPosition+1; 200 lv.setItemChecked(listViewPosition, true); 201 } 202 203 builder.setPositiveButton(R.string.allow_button, new DialogInterface.OnClickListener() { 204 @Override public void onClick(DialogInterface dialog, int id) { 205 int listViewPosition = lv.getCheckedItemPosition(); 206 int adapterPosition = listViewPosition-1; 207 String alias = ((adapterPosition >= 0) 208 ? adapter.getItem(adapterPosition) 209 : null); 210 finish(alias); 211 } 212 }); 213 } 214 builder.setTitle(title); 215 final Dialog dialog = builder.create(); 216 217 218 // getTargetPackage guarantees that the returned string is 219 // supplied by the system, so that an application can not 220 // spoof its package. 221 String pkg = mSender.getIntentSender().getTargetPackage(); 222 PackageManager pm = getPackageManager(); 223 CharSequence applicationLabel; 224 try { 225 applicationLabel = pm.getApplicationLabel(pm.getApplicationInfo(pkg, 0)).toString(); 226 } catch (PackageManager.NameNotFoundException e) { 227 applicationLabel = pkg; 228 } 229 String appMessage = String.format(res.getString(R.string.requesting_application), 230 applicationLabel); 231 232 String contextMessage = appMessage; 233 String host = getIntent().getStringExtra(KeyChain.EXTRA_HOST); 234 if (host != null) { 235 String hostString = host; 236 int port = getIntent().getIntExtra(KeyChain.EXTRA_PORT, -1); 237 if (port != -1) { 238 hostString += ":" + port; 239 } 240 String hostMessage = String.format(res.getString(R.string.requesting_server), 241 hostString); 242 if (contextMessage == null) { 243 contextMessage = hostMessage; 244 } else { 245 contextMessage += " " + hostMessage; 246 } 247 } 248 contextView.setText(contextMessage); 249 250 String installMessage = String.format(res.getString(R.string.install_new_cert_message), 251 Credentials.EXTENSION_PFX, Credentials.EXTENSION_P12); 252 TextView installText = (TextView) footer.findViewById(R.id.cert_chooser_install_message); 253 installText.setText(installMessage); 254 255 Button installButton = (Button) footer.findViewById(R.id.cert_chooser_install_button); 256 installButton.setOnClickListener(new View.OnClickListener() { 257 @Override public void onClick(View v) { 258 // remove dialog so that we will recreate with 259 // possibly new content after install returns 260 dialog.dismiss(); 261 Credentials.getInstance().install(KeyChainActivity.this); 262 } 263 }); 264 265 dialog.setOnCancelListener(new DialogInterface.OnCancelListener() { 266 @Override public void onCancel(DialogInterface dialog) { 267 finish(null); 268 } 269 }); 270 dialog.show(); 271 } 272 273 private class CertificateAdapter extends BaseAdapter { 274 private final List<String> mAliases; 275 private final List<String> mSubjects = new ArrayList<String>(); CertificateAdapter(List<String> aliases)276 private CertificateAdapter(List<String> aliases) { 277 mAliases = aliases; 278 mSubjects.addAll(Collections.nCopies(aliases.size(), (String) null)); 279 } getCount()280 @Override public int getCount() { 281 return mAliases.size(); 282 } getItem(int adapterPosition)283 @Override public String getItem(int adapterPosition) { 284 return mAliases.get(adapterPosition); 285 } getItemId(int adapterPosition)286 @Override public long getItemId(int adapterPosition) { 287 return adapterPosition; 288 } getView(final int adapterPosition, View view, ViewGroup parent)289 @Override public View getView(final int adapterPosition, View view, ViewGroup parent) { 290 ViewHolder holder; 291 if (view == null) { 292 LayoutInflater inflater = LayoutInflater.from(KeyChainActivity.this); 293 view = inflater.inflate(R.layout.cert_item, parent, false); 294 holder = new ViewHolder(); 295 holder.mAliasTextView = (TextView) view.findViewById(R.id.cert_item_alias); 296 holder.mSubjectTextView = (TextView) view.findViewById(R.id.cert_item_subject); 297 holder.mRadioButton = (RadioButton) view.findViewById(R.id.cert_item_selected); 298 view.setTag(holder); 299 } else { 300 holder = (ViewHolder) view.getTag(); 301 } 302 303 String alias = mAliases.get(adapterPosition); 304 305 holder.mAliasTextView.setText(alias); 306 307 String subject = mSubjects.get(adapterPosition); 308 if (subject == null) { 309 new CertLoader(adapterPosition, holder.mSubjectTextView).execute(); 310 } else { 311 holder.mSubjectTextView.setText(subject); 312 } 313 314 ListView lv = (ListView)parent; 315 int listViewCheckedItemPosition = lv.getCheckedItemPosition(); 316 int adapterCheckedItemPosition = listViewCheckedItemPosition-1; 317 holder.mRadioButton.setChecked(adapterPosition == adapterCheckedItemPosition); 318 return view; 319 } 320 321 private class CertLoader extends AsyncTask<Void, Void, String> { 322 private final int mAdapterPosition; 323 private final TextView mSubjectView; CertLoader(int adapterPosition, TextView subjectView)324 private CertLoader(int adapterPosition, TextView subjectView) { 325 mAdapterPosition = adapterPosition; 326 mSubjectView = subjectView; 327 } doInBackground(Void... params)328 @Override protected String doInBackground(Void... params) { 329 String alias = mAliases.get(mAdapterPosition); 330 byte[] bytes = mKeyStore.get(Credentials.USER_CERTIFICATE + alias); 331 if (bytes == null) { 332 return null; 333 } 334 InputStream in = new ByteArrayInputStream(bytes); 335 X509Certificate cert; 336 try { 337 CertificateFactory cf = CertificateFactory.getInstance("X.509"); 338 cert = (X509Certificate)cf.generateCertificate(in); 339 } catch (CertificateException ignored) { 340 return null; 341 } 342 // bouncycastle can handle the emailAddress OID of 1.2.840.113549.1.9.1 343 X500Principal subjectPrincipal = cert.getSubjectX500Principal(); 344 X509Name subjectName = X509Name.getInstance(subjectPrincipal.getEncoded()); 345 String subjectString = subjectName.toString(true, X509Name.DefaultSymbols); 346 return subjectString; 347 } onPostExecute(String subjectString)348 @Override protected void onPostExecute(String subjectString) { 349 mSubjects.set(mAdapterPosition, subjectString); 350 mSubjectView.setText(subjectString); 351 } 352 } 353 } 354 355 private static class ViewHolder { 356 TextView mAliasTextView; 357 TextView mSubjectTextView; 358 RadioButton mRadioButton; 359 } 360 onActivityResult(int requestCode, int resultCode, Intent data)361 @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { 362 switch (requestCode) { 363 case REQUEST_UNLOCK: 364 if (mKeyStore.isUnlocked()) { 365 mState = State.INITIAL; 366 showCertChooserDialog(); 367 } else { 368 // user must have canceled unlock, give up 369 mState = State.UNLOCK_CANCELED; 370 } 371 return; 372 default: 373 throw new AssertionError(); 374 } 375 } 376 finish(String alias)377 private void finish(String alias) { 378 if (alias == null) { 379 setResult(RESULT_CANCELED); 380 } else { 381 Intent result = new Intent(); 382 result.putExtra(Intent.EXTRA_TEXT, alias); 383 setResult(RESULT_OK, result); 384 } 385 IKeyChainAliasCallback keyChainAliasResponse 386 = IKeyChainAliasCallback.Stub.asInterface( 387 getIntent().getIBinderExtra(KeyChain.EXTRA_RESPONSE)); 388 if (keyChainAliasResponse != null) { 389 new ResponseSender(keyChainAliasResponse, alias).execute(); 390 return; 391 } 392 finish(); 393 } 394 395 private class ResponseSender extends AsyncTask<Void, Void, Void> { 396 private IKeyChainAliasCallback mKeyChainAliasResponse; 397 private String mAlias; ResponseSender(IKeyChainAliasCallback keyChainAliasResponse, String alias)398 private ResponseSender(IKeyChainAliasCallback keyChainAliasResponse, String alias) { 399 mKeyChainAliasResponse = keyChainAliasResponse; 400 mAlias = alias; 401 } doInBackground(Void... unused)402 @Override protected Void doInBackground(Void... unused) { 403 try { 404 if (mAlias != null) { 405 KeyChain.KeyChainConnection connection = KeyChain.bind(KeyChainActivity.this); 406 try { 407 connection.getService().setGrant(mSenderUid, mAlias, true); 408 } finally { 409 connection.close(); 410 } 411 } 412 mKeyChainAliasResponse.alias(mAlias); 413 } catch (InterruptedException ignored) { 414 Thread.currentThread().interrupt(); 415 Log.d(TAG, "interrupted while granting access", ignored); 416 } catch (Exception ignored) { 417 // don't just catch RemoteException, caller could 418 // throw back a RuntimeException across processes 419 // which we should protect against. 420 Log.e(TAG, "error while granting access", ignored); 421 } 422 return null; 423 } onPostExecute(Void unused)424 @Override protected void onPostExecute(Void unused) { 425 finish(); 426 } 427 } 428 onBackPressed()429 @Override public void onBackPressed() { 430 finish(null); 431 } 432 onSaveInstanceState(Bundle savedState)433 @Override protected void onSaveInstanceState(Bundle savedState) { 434 super.onSaveInstanceState(savedState); 435 if (mState != State.INITIAL) { 436 savedState.putSerializable(KEY_STATE, mState); 437 } 438 } 439 } 440