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