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.settings; 18 19 import android.app.AlertDialog; 20 import android.app.Dialog; 21 import android.app.Fragment; 22 import android.content.DialogInterface; 23 import android.net.http.SslCertificate; 24 import android.os.AsyncTask; 25 import android.os.Bundle; 26 import android.os.RemoteException; 27 import android.security.IKeyChainService; 28 import android.security.KeyChain; 29 import android.security.KeyChain.KeyChainConnection; 30 import android.view.LayoutInflater; 31 import android.view.View; 32 import android.view.ViewGroup; 33 import android.widget.AdapterView; 34 import android.widget.BaseAdapter; 35 import android.widget.Button; 36 import android.widget.CheckBox; 37 import android.widget.FrameLayout; 38 import android.widget.ListView; 39 import android.widget.ProgressBar; 40 import android.widget.TabHost; 41 import android.widget.TextView; 42 import java.security.cert.CertificateEncodingException; 43 import java.security.cert.X509Certificate; 44 import java.util.ArrayList; 45 import java.util.Collections; 46 import java.util.List; 47 import java.util.Set; 48 import org.apache.harmony.xnet.provider.jsse.TrustedCertificateStore; 49 50 public class TrustedCredentialsSettings extends Fragment { 51 52 private static final String TAG = "TrustedCredentialsSettings"; 53 54 private enum Tab { 55 SYSTEM("system", 56 R.string.trusted_credentials_system_tab, 57 R.id.system_tab, 58 R.id.system_progress, 59 R.id.system_list, 60 true), 61 USER("user", 62 R.string.trusted_credentials_user_tab, 63 R.id.user_tab, 64 R.id.user_progress, 65 R.id.user_list, 66 false); 67 68 private final String mTag; 69 private final int mLabel; 70 private final int mView; 71 private final int mProgress; 72 private final int mList; 73 private final boolean mCheckbox; 74 private Tab(String tag, int label, int view, int progress, int list, boolean checkbox) { 75 mTag = tag; 76 mLabel = label; 77 mView = view; 78 mProgress = progress; 79 mList = list; 80 mCheckbox = checkbox; 81 } 82 private Set<String> getAliases(TrustedCertificateStore store) { 83 switch (this) { 84 case SYSTEM: 85 return store.allSystemAliases(); 86 case USER: 87 return store.userAliases(); 88 } 89 throw new AssertionError(); 90 } 91 private boolean deleted(TrustedCertificateStore store, String alias) { 92 switch (this) { 93 case SYSTEM: 94 return !store.containsAlias(alias); 95 case USER: 96 return false; 97 } 98 throw new AssertionError(); 99 } 100 private int getButtonLabel(CertHolder certHolder) { 101 switch (this) { 102 case SYSTEM: 103 if (certHolder.mDeleted) { 104 return R.string.trusted_credentials_enable_label; 105 } 106 return R.string.trusted_credentials_disable_label; 107 case USER: 108 return R.string.trusted_credentials_remove_label; 109 } 110 throw new AssertionError(); 111 } 112 private int getButtonConfirmation(CertHolder certHolder) { 113 switch (this) { 114 case SYSTEM: 115 if (certHolder.mDeleted) { 116 return R.string.trusted_credentials_enable_confirmation; 117 } 118 return R.string.trusted_credentials_disable_confirmation; 119 case USER: 120 return R.string.trusted_credentials_remove_confirmation; 121 } 122 throw new AssertionError(); 123 } 124 private void postOperationUpdate(boolean ok, CertHolder certHolder) { 125 if (ok) { 126 if (certHolder.mTab.mCheckbox) { 127 certHolder.mDeleted = !certHolder.mDeleted; 128 } else { 129 certHolder.mAdapter.mCertHolders.remove(certHolder); 130 } 131 certHolder.mAdapter.notifyDataSetChanged(); 132 } else { 133 // bail, reload to reset to known state 134 certHolder.mAdapter.load(); 135 } 136 } 137 } 138 139 // be careful not to use this on the UI thread since it is does file operations 140 private final TrustedCertificateStore mStore = new TrustedCertificateStore(); 141 142 private TabHost mTabHost; 143 144 @Override public View onCreateView( 145 LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) { 146 mTabHost = (TabHost) inflater.inflate(R.layout.trusted_credentials, parent, false); 147 mTabHost.setup(); 148 addTab(Tab.SYSTEM); 149 // TODO add Install button on Tab.USER to go to CertInstaller like KeyChainActivity 150 addTab(Tab.USER); 151 return mTabHost; 152 } 153 154 private void addTab(Tab tab) { 155 TabHost.TabSpec systemSpec = mTabHost.newTabSpec(tab.mTag) 156 .setIndicator(getActivity().getString(tab.mLabel)) 157 .setContent(tab.mView); 158 mTabHost.addTab(systemSpec); 159 160 ListView lv = (ListView) mTabHost.findViewById(tab.mList); 161 final TrustedCertificateAdapter adapter = new TrustedCertificateAdapter(tab); 162 lv.setAdapter(adapter); 163 lv.setOnItemClickListener(new AdapterView.OnItemClickListener() { 164 @Override public void onItemClick(AdapterView<?> parent, View view, int pos, long id) { 165 showCertDialog(adapter.getItem(pos)); 166 } 167 }); 168 } 169 170 private class TrustedCertificateAdapter extends BaseAdapter { 171 private final List<CertHolder> mCertHolders = new ArrayList<CertHolder>(); 172 private final Tab mTab; 173 private TrustedCertificateAdapter(Tab tab) { 174 mTab = tab; 175 load(); 176 } 177 private void load() { 178 new AliasLoader().execute(); 179 } 180 @Override public int getCount() { 181 return mCertHolders.size(); 182 } 183 @Override public CertHolder getItem(int position) { 184 return mCertHolders.get(position); 185 } 186 @Override public long getItemId(int position) { 187 return position; 188 } 189 @Override public View getView(int position, View view, ViewGroup parent) { 190 ViewHolder holder; 191 if (view == null) { 192 LayoutInflater inflater = LayoutInflater.from(getActivity()); 193 view = inflater.inflate(R.layout.trusted_credential, parent, false); 194 holder = new ViewHolder(); 195 holder.mSubjectPrimaryView = (TextView) 196 view.findViewById(R.id.trusted_credential_subject_primary); 197 holder.mSubjectSecondaryView = (TextView) 198 view.findViewById(R.id.trusted_credential_subject_secondary); 199 holder.mCheckBox = (CheckBox) view.findViewById(R.id.trusted_credential_status); 200 view.setTag(holder); 201 } else { 202 holder = (ViewHolder) view.getTag(); 203 } 204 CertHolder certHolder = mCertHolders.get(position); 205 holder.mSubjectPrimaryView.setText(certHolder.mSubjectPrimary); 206 holder.mSubjectSecondaryView.setText(certHolder.mSubjectSecondary); 207 if (mTab.mCheckbox) { 208 holder.mCheckBox.setChecked(!certHolder.mDeleted); 209 holder.mCheckBox.setVisibility(View.VISIBLE); 210 } 211 return view; 212 }; 213 214 private class AliasLoader extends AsyncTask<Void, Integer, List<CertHolder>> { 215 ProgressBar mProgressBar; 216 View mList; 217 @Override protected void onPreExecute() { 218 View content = mTabHost.getTabContentView(); 219 mProgressBar = (ProgressBar) content.findViewById(mTab.mProgress); 220 mList = content.findViewById(mTab.mList); 221 mProgressBar.setVisibility(View.VISIBLE); 222 mList.setVisibility(View.GONE); 223 } 224 @Override protected List<CertHolder> doInBackground(Void... params) { 225 Set<String> aliases = mTab.getAliases(mStore); 226 int max = aliases.size(); 227 int progress = 0; 228 List<CertHolder> certHolders = new ArrayList<CertHolder>(max); 229 for (String alias : aliases) { 230 X509Certificate cert = (X509Certificate) mStore.getCertificate(alias, true); 231 certHolders.add(new CertHolder(mStore, 232 TrustedCertificateAdapter.this, 233 mTab, 234 alias, 235 cert)); 236 publishProgress(++progress, max); 237 } 238 Collections.sort(certHolders); 239 return certHolders; 240 } 241 @Override protected void onProgressUpdate(Integer... progressAndMax) { 242 int progress = progressAndMax[0]; 243 int max = progressAndMax[1]; 244 if (max != mProgressBar.getMax()) { 245 mProgressBar.setMax(max); 246 } 247 mProgressBar.setProgress(progress); 248 } 249 @Override protected void onPostExecute(List<CertHolder> certHolders) { 250 mCertHolders.clear(); 251 mCertHolders.addAll(certHolders); 252 notifyDataSetChanged(); 253 View content = mTabHost.getTabContentView(); 254 mProgressBar.setVisibility(View.GONE); 255 mList.setVisibility(View.VISIBLE); 256 mProgressBar.setProgress(0); 257 } 258 } 259 } 260 261 private static class CertHolder implements Comparable<CertHolder> { 262 private final TrustedCertificateStore mStore; 263 private final TrustedCertificateAdapter mAdapter; 264 private final Tab mTab; 265 private final String mAlias; 266 private final X509Certificate mX509Cert; 267 268 private final SslCertificate mSslCert; 269 private final String mSubjectPrimary; 270 private final String mSubjectSecondary; 271 private boolean mDeleted; 272 273 private CertHolder(TrustedCertificateStore store, 274 TrustedCertificateAdapter adapter, 275 Tab tab, 276 String alias, 277 X509Certificate x509Cert) { 278 mStore = store; 279 mAdapter = adapter; 280 mTab = tab; 281 mAlias = alias; 282 mX509Cert = x509Cert; 283 284 mSslCert = new SslCertificate(x509Cert); 285 286 String cn = mSslCert.getIssuedTo().getCName(); 287 String o = mSslCert.getIssuedTo().getOName(); 288 String ou = mSslCert.getIssuedTo().getUName(); 289 // if we have a O, use O as primary subject, secondary prefer CN over OU 290 // if we don't have an O, use CN as primary, empty secondary 291 // if we don't have O or CN, use DName as primary, empty secondary 292 if (!o.isEmpty()) { 293 if (!cn.isEmpty()) { 294 mSubjectPrimary = o; 295 mSubjectSecondary = cn; 296 } else { 297 mSubjectPrimary = o; 298 mSubjectSecondary = ou; 299 } 300 } else { 301 if (!cn.isEmpty()) { 302 mSubjectPrimary = cn; 303 mSubjectSecondary = ""; 304 } else { 305 mSubjectPrimary = mSslCert.getIssuedTo().getDName(); 306 mSubjectSecondary = ""; 307 } 308 } 309 mDeleted = mTab.deleted(mStore, mAlias); 310 } 311 @Override public int compareTo(CertHolder o) { 312 int primary = this.mSubjectPrimary.compareToIgnoreCase(o.mSubjectPrimary); 313 if (primary != 0) { 314 return primary; 315 } 316 return this.mSubjectSecondary.compareToIgnoreCase(o.mSubjectSecondary); 317 } 318 @Override public boolean equals(Object o) { 319 if (!(o instanceof CertHolder)) { 320 return false; 321 } 322 CertHolder other = (CertHolder) o; 323 return mAlias.equals(other.mAlias); 324 } 325 @Override public int hashCode() { 326 return mAlias.hashCode(); 327 } 328 } 329 330 private static class ViewHolder { 331 private TextView mSubjectPrimaryView; 332 private TextView mSubjectSecondaryView; 333 private CheckBox mCheckBox; 334 } 335 336 private void showCertDialog(final CertHolder certHolder) { 337 View view = certHolder.mSslCert.inflateCertificateView(getActivity()); 338 AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); 339 builder.setTitle(com.android.internal.R.string.ssl_certificate); 340 builder.setView(view); 341 builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { 342 @Override public void onClick(DialogInterface dialog, int id) { 343 dialog.dismiss(); 344 } 345 }); 346 final Dialog certDialog = builder.create(); 347 348 ViewGroup body = (ViewGroup) view.findViewById(com.android.internal.R.id.body); 349 LayoutInflater inflater = LayoutInflater.from(getActivity()); 350 Button removeButton = (Button) inflater.inflate(R.layout.trusted_credential_details, 351 body, 352 false); 353 body.addView(removeButton); 354 removeButton.setText(certHolder.mTab.getButtonLabel(certHolder)); 355 removeButton.setOnClickListener(new View.OnClickListener() { 356 @Override public void onClick(View v) { 357 AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); 358 builder.setMessage(certHolder.mTab.getButtonConfirmation(certHolder)); 359 builder.setPositiveButton( 360 android.R.string.yes, new DialogInterface.OnClickListener() { 361 @Override public void onClick(DialogInterface dialog, int id) { 362 new AliasOperation(certHolder).execute(); 363 dialog.dismiss(); 364 certDialog.dismiss(); 365 } 366 }); 367 builder.setNegativeButton( 368 android.R.string.no, new DialogInterface.OnClickListener() { 369 @Override public void onClick(DialogInterface dialog, int id) { 370 dialog.cancel(); 371 } 372 }); 373 AlertDialog alert = builder.create(); 374 alert.show(); 375 } 376 }); 377 378 certDialog.show(); 379 } 380 381 private class AliasOperation extends AsyncTask<Void, Void, Boolean> { 382 private final CertHolder mCertHolder; 383 private AliasOperation(CertHolder certHolder) { 384 mCertHolder = certHolder; 385 } 386 @Override protected Boolean doInBackground(Void... params) { 387 try { 388 KeyChainConnection keyChainConnection = KeyChain.bind(getActivity()); 389 IKeyChainService service = keyChainConnection.getService(); 390 try { 391 if (mCertHolder.mDeleted) { 392 byte[] bytes = mCertHolder.mX509Cert.getEncoded(); 393 service.installCaCertificate(bytes); 394 return true; 395 } else { 396 return service.deleteCaCertificate(mCertHolder.mAlias); 397 } 398 } finally { 399 keyChainConnection.close(); 400 } 401 } catch (CertificateEncodingException e) { 402 return false; 403 } catch (IllegalStateException e) { 404 // used by installCaCertificate to report errors 405 return false; 406 } catch (RemoteException e) { 407 return false; 408 } catch (InterruptedException e) { 409 Thread.currentThread().interrupt(); 410 return false; 411 } 412 } 413 @Override protected void onPostExecute(Boolean ok) { 414 mCertHolder.mTab.postOperationUpdate(ok, mCertHolder); 415 } 416 } 417 } 418