1 /* 2 * Copyright (C) 2009 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.certinstaller; 18 19 import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS; 20 21 import android.app.Activity; 22 import android.app.AlertDialog; 23 import android.app.Dialog; 24 import android.app.ProgressDialog; 25 import android.content.ActivityNotFoundException; 26 import android.content.Context; 27 import android.content.Intent; 28 import android.content.pm.ApplicationInfo; 29 import android.content.pm.PackageManager; 30 import android.os.AsyncTask; 31 import android.os.Bundle; 32 import android.os.Process; 33 import android.security.Credentials; 34 import android.security.KeyChain; 35 import android.security.KeyChain.KeyChainConnection; 36 import android.text.TextUtils; 37 import android.util.Log; 38 import android.util.Slog; 39 import android.view.View; 40 import android.view.ViewGroup; 41 import android.widget.EditText; 42 import android.widget.RadioGroup; 43 import android.widget.Toast; 44 45 import com.android.internal.annotations.VisibleForTesting; 46 47 import java.io.Serializable; 48 import java.util.HashMap; 49 import java.util.Map; 50 51 /** 52 * Installs certificates to the system keystore. 53 */ 54 public class CertInstaller extends Activity { 55 private static final String TAG = "CertInstaller"; 56 57 private static final int STATE_INIT = 1; 58 private static final int STATE_RUNNING = 2; 59 private static final int STATE_PAUSED = 3; 60 61 private static final int NAME_CREDENTIAL_DIALOG = 1; 62 private static final int PKCS12_PASSWORD_DIALOG = 2; 63 private static final int PROGRESS_BAR_DIALOG = 3; 64 private static final int REDIRECT_CA_CERTIFICATE_DIALOG = 4; 65 private static final int SELECT_CERTIFICATE_USAGE_DIALOG = 5; 66 private static final int INVALID_CERTIFICATE_DIALOG = 6; 67 68 private static final int REQUEST_SYSTEM_INSTALL_CODE = 1; 69 70 // key to states Bundle 71 private static final String NEXT_ACTION_KEY = "na"; 72 73 private final ViewHelper mView = new ViewHelper(); 74 75 private int mState; 76 private CredentialHelper mCredentials; 77 private MyAction mNextAction; 78 createCredentialHelper(Intent intent)79 private CredentialHelper createCredentialHelper(Intent intent) { 80 try { 81 Bundle bundle = intent.getExtras(); 82 if (bundle == null) { 83 return new CredentialHelper(); 84 } else { 85 int size = bundle.size(); 86 Log.d(TAG, "# extras: " + size); 87 88 String name = bundle.getString(KeyChain.EXTRA_NAME); 89 bundle.remove(KeyChain.EXTRA_NAME); 90 91 String referrer = bundle.getString(Intent.EXTRA_REFERRER); 92 bundle.remove(Intent.EXTRA_REFERRER); 93 94 String certUsageSelected = bundle.getString(Credentials.EXTRA_CERTIFICATE_USAGE); 95 bundle.remove(Credentials.EXTRA_CERTIFICATE_USAGE); 96 97 int uid = bundle.getInt(Credentials.EXTRA_INSTALL_AS_UID, Process.INVALID_UID); 98 bundle.remove(Credentials.EXTRA_INSTALL_AS_UID); 99 100 Map<String, byte[]> byteMap = new HashMap<>(); 101 for (String key : bundle.keySet()) { 102 byte[] bytes = bundle.getByteArray(key); 103 byteMap.put(key, bytes); 104 } 105 return new CredentialHelper(byteMap, name, referrer, certUsageSelected, uid); 106 } 107 } catch (Throwable t) { 108 Log.w(TAG, "createCredentialHelper", t); 109 toastErrorAndFinish(R.string.invalid_cert); 110 return new CredentialHelper(); 111 } 112 } 113 114 @Override onCreate(Bundle savedStates)115 protected void onCreate(Bundle savedStates) { 116 super.onCreate(savedStates); 117 getWindow().addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS); 118 119 mCredentials = createCredentialHelper(getIntent()); 120 121 mState = (savedStates == null) ? STATE_INIT : STATE_RUNNING; 122 123 if (mState == STATE_INIT) { 124 if (!mCredentials.containsAnyRawData()) { 125 toastErrorAndFinish(R.string.no_cert_to_saved); 126 finish(); 127 } else { 128 if (installingCaCertificate()) { 129 extractPkcs12OrInstall(); 130 } else { 131 if (mCredentials.hasUserCertificate() && !mCredentials.hasPrivateKey()) { 132 toastErrorAndFinish(R.string.action_missing_private_key); 133 } else if (mCredentials.hasPrivateKey() && !mCredentials.hasUserCertificate()) { 134 toastErrorAndFinish(R.string.action_missing_user_cert); 135 } else { 136 extractPkcs12OrInstall(); 137 } 138 } 139 } 140 } else { 141 mCredentials.onRestoreStates(savedStates); 142 mNextAction = (MyAction) 143 savedStates.getSerializable(NEXT_ACTION_KEY); 144 } 145 } 146 installingCaCertificate()147 private boolean installingCaCertificate() { 148 return mCredentials.hasCaCerts() && !mCredentials.hasPrivateKey() && 149 !mCredentials.hasUserCertificate(); 150 } 151 152 @Override onResume()153 protected void onResume() { 154 super.onResume(); 155 156 if (mState == STATE_INIT) { 157 mState = STATE_RUNNING; 158 } else { 159 if (mNextAction != null) { 160 mNextAction.run(this); 161 } 162 } 163 } 164 165 @Override onPause()166 protected void onPause() { 167 super.onPause(); 168 mState = STATE_PAUSED; 169 } 170 171 @Override onSaveInstanceState(Bundle outStates)172 protected void onSaveInstanceState(Bundle outStates) { 173 super.onSaveInstanceState(outStates); 174 mCredentials.onSaveStates(outStates); 175 if (mNextAction != null) { 176 outStates.putSerializable(NEXT_ACTION_KEY, mNextAction); 177 } 178 } 179 180 @Override onCreateDialog(int dialogId)181 protected Dialog onCreateDialog (int dialogId) { 182 switch (dialogId) { 183 case PKCS12_PASSWORD_DIALOG: 184 return createPkcs12PasswordDialog(); 185 186 case NAME_CREDENTIAL_DIALOG: 187 return createNameCertificateDialog(); 188 189 case PROGRESS_BAR_DIALOG: 190 ProgressDialog dialog = new ProgressDialog(this); 191 dialog.setMessage(getString(R.string.extracting_pkcs12)); 192 dialog.setIndeterminate(true); 193 dialog.setCancelable(false); 194 return dialog; 195 196 case REDIRECT_CA_CERTIFICATE_DIALOG: 197 return createRedirectCaCertificateDialog(); 198 199 case SELECT_CERTIFICATE_USAGE_DIALOG: 200 return createSelectCertificateUsageDialog(); 201 202 case INVALID_CERTIFICATE_DIALOG: 203 return createInvalidCertificateDialog(); 204 205 default: 206 return null; 207 } 208 } 209 210 @Override onActivityResult(int requestCode, int resultCode, Intent data)211 protected void onActivityResult(int requestCode, int resultCode, Intent data) { 212 switch (requestCode) { 213 case REQUEST_SYSTEM_INSTALL_CODE: 214 if (resultCode != RESULT_OK) { 215 Log.d(TAG, "credential not saved, err: " + resultCode); 216 toastErrorAndFinish(R.string.cert_not_saved); 217 return; 218 } 219 220 Log.d(TAG, "credential is added: " + mCredentials.getName()); 221 if (mCredentials.getCertUsageSelected().equals(Credentials.CERTIFICATE_USAGE_WIFI)) { 222 Toast.makeText(this, R.string.wifi_cert_is_added, Toast.LENGTH_LONG).show(); 223 } else { 224 Toast.makeText(this, R.string.user_cert_is_added, Toast.LENGTH_LONG).show(); 225 } 226 setResult(RESULT_OK); 227 finish(); 228 break; 229 default: 230 Log.w(TAG, "unknown request code: " + requestCode); 231 finish(); 232 break; 233 } 234 } 235 extractPkcs12OrInstall()236 private void extractPkcs12OrInstall() { 237 if (mCredentials.hasPkcs12KeyStore()) { 238 if (mCredentials.hasPassword()) { 239 showDialog(PKCS12_PASSWORD_DIALOG); 240 } else { 241 new Pkcs12ExtractAction("").run(this); 242 } 243 } else { 244 if (mCredentials.calledBySettings()) { 245 MyAction action = new InstallOthersAction(); 246 action.run(this); 247 } else { 248 createRedirectOrSelectUsageDialog(); 249 } 250 } 251 } 252 253 private class InstallVpnAndAppsTrustAnchorsTask extends AsyncTask<Void, Void, Boolean> { 254 255 @Override doInBackground(Void... unused)256 protected Boolean doInBackground(Void... unused) { 257 try { 258 try (KeyChainConnection keyChainConnection = KeyChain.bind(CertInstaller.this)) { 259 return mCredentials.installVpnAndAppsTrustAnchors(CertInstaller.this, 260 keyChainConnection.getService()); 261 } 262 } catch (InterruptedException e) { 263 Thread.currentThread().interrupt(); 264 return false; 265 } 266 } 267 268 @Override onPostExecute(Boolean success)269 protected void onPostExecute(Boolean success) { 270 if (success) { 271 Toast.makeText(getApplicationContext(), R.string.ca_cert_is_added, 272 Toast.LENGTH_LONG).show(); 273 setResult(RESULT_OK); 274 } 275 finish(); 276 } 277 } 278 installOthers()279 private void installOthers() { 280 // Sanity check: Check that there's either: 281 // * A private key AND a user certificate, or 282 // * A CA cert. 283 boolean hasPrivateKeyAndUserCertificate = 284 mCredentials.hasPrivateKey() && mCredentials.hasUserCertificate(); 285 boolean hasCaCertificate = mCredentials.hasCaCerts(); 286 Log.d(TAG, 287 String.format( 288 "Attempting credentials installation, has ca cert? %b, has user cert? %b", 289 hasCaCertificate, hasPrivateKeyAndUserCertificate)); 290 if (!(hasPrivateKeyAndUserCertificate || hasCaCertificate)) { 291 finish(); 292 return; 293 } 294 295 if (validCertificateSelected()) { 296 installCertificateOrShowNameDialog(); 297 } else { 298 showDialog(INVALID_CERTIFICATE_DIALOG); 299 } 300 } 301 validCertificateSelected()302 private boolean validCertificateSelected() { 303 switch (mCredentials.getCertUsageSelected()) { 304 case Credentials.CERTIFICATE_USAGE_CA: 305 return mCredentials.hasOnlyVpnAndAppsTrustAnchors(); 306 case Credentials.CERTIFICATE_USAGE_USER: 307 return mCredentials.hasUserCertificate() 308 && !mCredentials.hasOnlyVpnAndAppsTrustAnchors(); 309 case Credentials.CERTIFICATE_USAGE_WIFI: 310 return true; 311 default: 312 return false; 313 } 314 } 315 installCertificateOrShowNameDialog()316 private void installCertificateOrShowNameDialog() { 317 if (!mCredentials.hasAnyForSystemInstall()) { 318 toastErrorAndFinish(R.string.no_cert_to_saved); 319 } else if (mCredentials.hasOnlyVpnAndAppsTrustAnchors()) { 320 // If there's only a CA certificate to install, then it's going to be used 321 // as a trust anchor. Install it and skip importing to Keystore. 322 323 // more work to do, don't finish just yet 324 new InstallVpnAndAppsTrustAnchorsTask().execute(); 325 } else { 326 // Name is required if installing User certificate 327 showDialog(NAME_CREDENTIAL_DIALOG); 328 } 329 } 330 extractPkcs12InBackground(final String password)331 private void extractPkcs12InBackground(final String password) { 332 // show progress bar and extract certs in a background thread 333 showDialog(PROGRESS_BAR_DIALOG); 334 335 new AsyncTask<Void,Void,Boolean>() { 336 @Override protected Boolean doInBackground(Void... unused) { 337 return mCredentials.extractPkcs12(password); 338 } 339 @Override protected void onPostExecute(Boolean success) { 340 MyAction action = new OnExtractionDoneAction(success); 341 if (mState == STATE_PAUSED) { 342 // activity is paused; run it in next onResume() 343 mNextAction = action; 344 } else { 345 action.run(CertInstaller.this); 346 } 347 } 348 }.execute(); 349 } 350 onExtractionDone(boolean success)351 private void onExtractionDone(boolean success) { 352 mNextAction = null; 353 removeDialog(PROGRESS_BAR_DIALOG); 354 if (success) { 355 removeDialog(PKCS12_PASSWORD_DIALOG); 356 if (mCredentials.calledBySettings()) { 357 if (validCertificateSelected()) { 358 installCertificateOrShowNameDialog(); 359 } else { 360 showDialog(INVALID_CERTIFICATE_DIALOG); 361 } 362 } else { 363 createRedirectOrSelectUsageDialog(); 364 } 365 } else { 366 showDialog(PKCS12_PASSWORD_DIALOG); 367 mView.setText(R.id.credential_password, ""); 368 mView.showError(R.string.password_error); 369 } 370 } 371 createRedirectOrSelectUsageDialog()372 private void createRedirectOrSelectUsageDialog() { 373 if (mCredentials.hasOnlyVpnAndAppsTrustAnchors()) { 374 showDialog(REDIRECT_CA_CERTIFICATE_DIALOG); 375 } else { 376 showDialog(SELECT_CERTIFICATE_USAGE_DIALOG); 377 } 378 } 379 getCallingAppLabel()380 public CharSequence getCallingAppLabel() { 381 final String callingPkg = mCredentials.getReferrer(); 382 if (callingPkg == null) { 383 Log.e(TAG, "Cannot get calling calling AppPackage"); 384 return null; 385 } 386 387 final PackageManager pm = getPackageManager(); 388 final ApplicationInfo appInfo; 389 try { 390 appInfo = pm.getApplicationInfo(callingPkg, PackageManager.MATCH_DISABLED_COMPONENTS); 391 } catch (PackageManager.NameNotFoundException e) { 392 Log.e(TAG, "Unable to find info for package: " + callingPkg); 393 return null; 394 } 395 396 return appInfo.loadLabel(pm); 397 } 398 createRedirectCaCertificateDialog()399 private Dialog createRedirectCaCertificateDialog() { 400 final String message = getString( 401 R.string.redirect_ca_certificate_with_app_info_message, getCallingAppLabel()); 402 Dialog d = new AlertDialog.Builder(this) 403 .setTitle(R.string.redirect_ca_certificate_title) 404 .setMessage(message) 405 .setPositiveButton(R.string.redirect_ca_certificate_close_button, 406 (dialog, id) -> toastErrorAndFinish(R.string.cert_not_saved)) 407 .create(); 408 d.setOnCancelListener(dialog -> toastErrorAndFinish(R.string.cert_not_saved)); 409 return d; 410 } 411 createSelectCertificateUsageDialog()412 private Dialog createSelectCertificateUsageDialog() { 413 ViewGroup view = (ViewGroup) View.inflate(this, R.layout.select_certificate_usage_dialog, 414 null); 415 mView.setView(view); 416 417 RadioGroup radioGroup = view.findViewById(R.id.certificate_usage); 418 radioGroup.setOnCheckedChangeListener((group, checkedId) -> { 419 switch (checkedId) { 420 case R.id.user_certificate: 421 mCredentials.setCertUsageSelectedAndUid(Credentials.CERTIFICATE_USAGE_USER); 422 break; 423 case R.id.wifi_certificate: 424 mCredentials.setCertUsageSelectedAndUid(Credentials.CERTIFICATE_USAGE_WIFI); 425 default: 426 Slog.i(TAG, "Unknown selection for scope"); 427 } 428 }); 429 430 431 final Context appContext = getApplicationContext(); 432 Dialog d = new AlertDialog.Builder(this) 433 .setView(view) 434 .setPositiveButton(android.R.string.ok, (dialog, id) -> { 435 showDialog(NAME_CREDENTIAL_DIALOG); 436 }) 437 .setNegativeButton(android.R.string.cancel, 438 (dialog, id) -> toastErrorAndFinish(R.string.cert_not_saved)) 439 .create(); 440 d.setOnCancelListener(dialog -> toastErrorAndFinish(R.string.cert_not_saved)); 441 return d; 442 } 443 createInvalidCertificateDialog()444 private Dialog createInvalidCertificateDialog() { 445 Dialog d = new AlertDialog.Builder(this) 446 .setTitle(R.string.invalid_certificate_title) 447 .setMessage(getString(R.string.invalid_certificate_message, 448 getCertificateUsageName())) 449 .setPositiveButton(R.string.invalid_certificate_close_button, 450 (dialog, id) -> toastErrorAndFinish(R.string.cert_not_saved)) 451 .create(); 452 d.setOnCancelListener(dialog -> finish()); 453 return d; 454 } 455 getCertificateUsageName()456 String getCertificateUsageName() { 457 switch (mCredentials.getCertUsageSelected()) { 458 case Credentials.CERTIFICATE_USAGE_CA: 459 return getString(R.string.ca_certificate); 460 case Credentials.CERTIFICATE_USAGE_USER: 461 return getString(R.string.user_certificate); 462 case Credentials.CERTIFICATE_USAGE_WIFI: 463 return getString(R.string.wifi_certificate); 464 default: 465 return getString(R.string.certificate); 466 } 467 } 468 createPkcs12PasswordDialog()469 private Dialog createPkcs12PasswordDialog() { 470 View view = View.inflate(this, R.layout.password_dialog, null); 471 mView.setView(view); 472 if (mView.getHasEmptyError()) { 473 mView.showError(R.string.password_empty_error); 474 mView.setHasEmptyError(false); 475 } 476 477 String title = mCredentials.getName(); 478 title = TextUtils.isEmpty(title) 479 ? getString(R.string.pkcs12_password_dialog_title) 480 : getString(R.string.pkcs12_file_password_dialog_title, title); 481 Dialog d = new AlertDialog.Builder(this) 482 .setView(view) 483 .setTitle(title) 484 .setPositiveButton(android.R.string.ok, (dialog, id) -> { 485 String password = mView.getText(R.id.credential_password); 486 mNextAction = new Pkcs12ExtractAction(password); 487 mNextAction.run(CertInstaller.this); 488 }) 489 .setNegativeButton(android.R.string.cancel, 490 (dialog, id) -> toastErrorAndFinish(R.string.cert_not_saved)) 491 .create(); 492 d.setOnCancelListener(dialog -> toastErrorAndFinish(R.string.cert_not_saved)); 493 return d; 494 } 495 createNameCertificateDialog()496 private Dialog createNameCertificateDialog() { 497 ViewGroup view = (ViewGroup) View.inflate(this, R.layout.name_certificate_dialog, null); 498 mView.setView(view); 499 if (mView.getHasEmptyError()) { 500 mView.showError(R.string.name_empty_error); 501 mView.setHasEmptyError(false); 502 } 503 final EditText nameInput = view.findViewById(R.id.certificate_name); 504 nameInput.setText(getDefaultName()); 505 nameInput.selectAll(); 506 final Context appContext = getApplicationContext(); 507 508 Dialog d = new AlertDialog.Builder(this) 509 .setView(view) 510 .setTitle(R.string.name_credential_dialog_title) 511 .setPositiveButton(android.R.string.ok, (dialog, id) -> { 512 String name = mView.getText(R.id.certificate_name); 513 if (TextUtils.isEmpty(name)) { 514 mView.setHasEmptyError(true); 515 removeDialog(NAME_CREDENTIAL_DIALOG); 516 showDialog(NAME_CREDENTIAL_DIALOG); 517 } else { 518 removeDialog(NAME_CREDENTIAL_DIALOG); 519 mCredentials.setName(name); 520 installCertificateToKeystore(appContext); 521 } 522 }) 523 .setNegativeButton(android.R.string.cancel, 524 (dialog, id) -> toastErrorAndFinish(R.string.cert_not_saved)) 525 .create(); 526 d.setOnCancelListener(dialog -> toastErrorAndFinish(R.string.cert_not_saved)); 527 return d; 528 } 529 installCertificateToKeystore(Context context)530 private void installCertificateToKeystore(Context context) { 531 try { 532 startActivityForResult( 533 mCredentials.createSystemInstallIntent(context), 534 REQUEST_SYSTEM_INSTALL_CODE); 535 } catch (ActivityNotFoundException e) { 536 Log.w(TAG, "installCertificateToKeystore(): ", e); 537 toastErrorAndFinish(R.string.cert_not_saved); 538 } 539 } 540 getDefaultName()541 private String getDefaultName() { 542 String name = mCredentials.getName(); 543 if (TextUtils.isEmpty(name)) { 544 return null; 545 } else { 546 // remove the extension from the file name 547 int index = name.lastIndexOf("."); 548 if (index > 0) name = name.substring(0, index); 549 return name; 550 } 551 } 552 toastErrorAndFinish(int msgId)553 private void toastErrorAndFinish(int msgId) { 554 Toast.makeText(this, msgId, Toast.LENGTH_SHORT).show(); 555 finish(); 556 } 557 558 private interface MyAction extends Serializable { run(CertInstaller host)559 void run(CertInstaller host); 560 } 561 562 private static class Pkcs12ExtractAction implements MyAction { 563 private final String mPassword; 564 private transient boolean hasRun; 565 Pkcs12ExtractAction(String password)566 Pkcs12ExtractAction(String password) { 567 mPassword = password; 568 } 569 run(CertInstaller host)570 public void run(CertInstaller host) { 571 if (hasRun) { 572 return; 573 } 574 hasRun = true; 575 host.extractPkcs12InBackground(mPassword); 576 } 577 } 578 579 private static class InstallOthersAction implements MyAction { run(CertInstaller host)580 public void run(CertInstaller host) { 581 host.mNextAction = null; 582 host.installOthers(); 583 } 584 } 585 586 private static class OnExtractionDoneAction implements MyAction { 587 private final boolean mSuccess; 588 OnExtractionDoneAction(boolean success)589 OnExtractionDoneAction(boolean success) { 590 mSuccess = success; 591 } 592 run(CertInstaller host)593 public void run(CertInstaller host) { 594 host.onExtractionDone(mSuccess); 595 } 596 } 597 598 @VisibleForTesting getCredentials()599 public CredentialHelper getCredentials() { 600 return mCredentials; 601 } 602 } 603